"Объясни другому - поймешь сам" ( народная примета ) Миксер Объектов (или как научиться создавать Объектно Ориентированные Программы) [версия 1] К читателю: Идея изложения данного материала возникла после создания мной пакета Object miXMax (он предназначен для создания графического интерфейса). Дело в том, что для создания такого пакета мне потребовалось разобраться, что такое Объектно Ориентированное Программирование. К тому времени я уже довольно свободно обращался с классами как таковыми, но работали они как обычные структуры. Вся попадавшаяся мне литература о С++ ограничивалась описанием классов (его свойств и способностей), после чего следовала многозначительная фраза типа "Cоздание О.О.Программ требует нетрадиционного подхода к программированию". И все. Но что это за "нетрадиционный подход" оставалось загадкой. Поэтому я попытался в краткой форме раскрыть эту тайну для Вас. Материал, изложенный в этой книге, предназначается профессионалу, хорошо разбирающемуся в объектах, но которому еще не достает знаний в области создания Объектно Ориентированных Программ (в дальнейшем называемых О.О.программы) в целом. Я расскажу о том, как создать настоящий О.О.пакет и поделюсь своими мыслями о разработке графического интерфейса с помощью методов О.О.П. Что из себя представляет этот интерфейс, Вы можете узнать, запустив демонстрационную программу (находящуюся на этой же дискете). Что же касается новичков - то им будет сложно освоить эти страницы без предварительной подготовки. Я бы посоветовал Вам сначала прочитать какую-либо литературу, объясняющую, что такое класс (объект) и с чем его едят. Здесь же я только кратко касаюсь этого аспекта. Но если Вы решили продолжать чтение (что все-таки я не могу не приветствовать), то желаю удачи ! Внимание. "Внутренняя кухня" классов справедлива для компиляторов фирмы Borland. Если у Вас под рукой имеется описание к Turbo Vision (TV) фирмы Borland (для Паскаля или СИ), то рекомендую изучать эти две книги вместе (одно дополняет другое). В моей книге показана внутренняя структура пакета подобного TV фирмы Borland. 05.08.92 Содержание. Глава 1 = Основные шаги = Что такое О.О.Программирование............................... Управление программой........................................ Иерархия Глава 2 = Управление объектами = Как идет обработка событий................................... Два пути разработки классов.................................. Модальность.................................................. Фоновые операции............................................. Глава 3 = Как это делается = Сохранение объектов в потоке и загрузка объектов из потока... * Как изобрести велосипед...................................... Коллекции.................................................... Работа с мышкой и клавиатурой................................ Работа с дополнительной (Expanded) памятью................... * Pабота в защищенном pежиме по пpотоколу DPMI................. * Параллельные процессы........................................ Глава 4 = Немного о графике = Основные элементы............................................ Работа с экраном............................................. * Окно......................................................... Скроллинг текста в графическом режиме........................ PCX-формат................................................... * Перекрытие элементов на экране............................... Глава 5 = В двух словах = Множественное наследование................................... Отладка...................................................... Turbo Vision................................................. * Объекты на продажу........................................... ---- Пункты, помеченные символом * (звездочка) включены во вторую версию книги. ДИРЕКТОРИЯ TGV Файлы: readme TGV.exe --¬ *.pfl --+- демонстрационная программа к тексту; *.res --+ egavga.bgi --- bratemse.sys - драйвер expanded памяти; ems40.sys - драйвер expanded памяти; msmouse.com - драйвер мыши; ************************* Лицензионные права *********************** * * * Авторские права защищены. Москва (с) 1992. * * ПРИ ПЕРЕПЕЧАТКЕ, ИСПОЛЬЗОВАНИИ КОДА, ПРИВЕДЕННОГО В КНИГЕ, * * ССЫЛКА ОБЯЗАТЕЛЬНА. * * * ******************************************************************** * * * ! НЕ РАЗРЕШАЕТСЯ распространение книги отдельными главами ! * * ! и без демонстрационной программы ! * * * ******************************************************************** ******************************************************************** * * * НЕ РАЗРЕШАЕТСЯ вносить исправления в текст без согласия автора. * * * ******************************************************************** Все замечания принимаются по тел. (095)459-94-07 (9.00-17.00). Кривозубов Дмитрий Юрьевич (если меня не окажется по данному телефону, то спpосите как со мной связаться) Глава 1. Основные шаги. Что такое О.О.Программирование ? Понять, что такое объекты и создавать их, это еще не все, хотя и много. Нам нужно, вдобавок, научиться писать настоящие О.О.Программы, используя их (объекты). Если объекты вставлять в обычную программу, то большого выигрыша не будет, но зато получим большой по объему выполняемый код. Но давайте по порядку. Итак, какие же основные свойства самого объекта позволяют создавать "необычные" программы ? На мой взгляд их два - одно рассмотрим сейчас, другое - немного позже. Первое - это наследование. +---------------+ В этом смысле существует два основных понятия: | наследование | предок и потомок. Предок еще может называться +---------------+ базовым классом. Пожалуйста, не путайте базовый класс с абстрактным и наоборот. Абстрактный класс - всегда базовый, а базовый - не всегда абстрактный. Но в чем же различие ? Основное в том, что экземпляр абстрактного класса никогда не создается - это только проект потомка, его заготовка - ничто с точки зрения выполняемого кода. Если Вы попытаетесь создать экземпляр абстрактного класса, то компилятор откажется это сделать, выдав ошибку. Базовый (не абстрактный) класс - это полноценный объект, экземпляр которого можно успешно создать. Наследование позволяет передавать свойства предка потомкам. Поэтому если Вы заранее продумаете иерархию (хочу предупредить - это сложно - увидеть сразу всю цепочку объектов), то в базовый класс можно внести много полезных функций, которые пригодятся позднее. Попытаюсь пояснить: class people { char Sex[10]; -пол; char Name[10]; -имя; people(char* aSex, char* aName) -конструктор; {strcpy(Sex,aSex); strcpy(Name,aName); } void draw(){cout< draw(); Alex -> draw(); Nata -> draw(); Lena -> draw(); } В результате на экране будут выведены все имена с обозначением пола. Этот маленький пример хорошо иллюстрирует принцип: один раз создал - много раз использовал. О втором свойстве, как я обещал, будет рассказано позже. А сейчас давайте пока забудем об объектах и попробуем разобраться в технике программирования в целом. +-----------+ Первым и основным шагом на пути к созданию | связанные | О.О.Программ является понимание динамических структур | списки | данных. Этот вопрос освещен в книге Н.Вирта +-----------+ "Алгоритмы + структуры данных = программа". Мы же рассмотрим частный случай динамических структур данных - связанные списки. Вы, наверное, слышали о них, "cписки" используются в коде, написанном и на обычном СИ. Но вся мощь их раскрывается только при создании О.О.Программ. Что же такое связанный список ? В общих словах - это цепочка узлов (элементов), связанных между собой (посредством указателей) последовательно, т.е. первый указывает на второй, второй на третий и т.д. struct node { next* node; -указатель на следующий узел node; } ---------¬ ---------¬ ---------¬ ¦ node 1 ¦ ¦ node 2 ¦ ¦ node 3 ¦ ¦ next --+---->¦ next --+---->¦ next --+-> = 0 L--------- L--------- L--------- Рис 2а. Если указатель последнего элемента будет указывать на первый элемент, то получаем замкнутый связанный список. ---------¬ ---------¬ ---------¬ ¦ node 1 ¦ ¦ node 2 ¦ ¦ node 3 ¦ -->¦ next --+---->¦ next --+---->¦ next --+--¬ ¦ L--------- L--------- L--------- ¦ L--------------------------------------------- Рис 2б. (ярким примером замкнутого связанного списка является меню) Но в данной реализации этот связанный список ничего нам не дает, поэтому несколько модернизируем узел node, добавив в него еще одно поле obj. struct node { next* node; void* obj } Поле obj - указатель на какой-нибудь объект. ---------------------------------------------¬ ¦ ---------¬ ---------¬ ---------¬ ¦ ¦ ¦ node 1 ¦ ¦ node 2 ¦ ¦ node 3 ¦ ¦ L->¦ next --+---->¦ next --+---->¦ next --+--- ¦ obj ¦ ¦ obj ¦ ¦ obj ¦ L---T----- L---T----- L---T----- ¦ ¦ ¦ V V V object 1 object 2 object 3 Рис 3. Теперь с помощью операторов цикла (for, while, do) можно пройтись по списку и произвести с объектами, связанными с узлами, необходимые действия. Как видно из последнего рисунка, узел node и объект object - разные элементы программы. А нельзя ли вообще отказаться от узла node (ведь на его создание тратится время, а на размещение - память, мало того, он не несет никакой полезной нагрузки, кроме двух указателей, один из которых (а именно obj) явно лишний) ? Давайте поле next вставим в базовый класс и каждый потомок унаследует его. Так и сделаем: class people { people* next; -указывает на подобного себе; char Sex[10]; -пол; char Name[10]; -имя; people(char* aSex, char* aName) -конструктор; {next = 0; strcpy(Sex,aSex); strcpy(Name,aName); } void draw(){cout< next = Alex; Alex -> next = Nata; Nata -> next = Lena; people* m = Dima; // -создаем указатель типа people и присваиваем //ему указатель на первый наш объект; while(m->next) //в цикле последовательно проходим по { //списку - от Dima до Lena и у каждого m->draw(); //вызываем функцию draw(); m=m->next } } Когда я открыл для себя это свойство списков применительно к объектам, моему восторгу не было предела, и все мои программы превратились в сплошные операторы do, while и for. Да, это действительно здорово, но что произойдет, если у объекта woman будет своя функция draw(), a у man - своя ? Например, woman::draw() будет выводить информацию с первой позиции экрана, а man::draw() - c сороковой. void woman::draw() <- перегрузка (замена) функции draw предка !!! { gotoxy(1,1); people::draw(); } void man::draw() <- перегрузка (замена) функции draw предка !!! { gotoxy(40,1); people::draw(); } Запустим программу, и если вы ожидаете, что все пойдет "как по маслу", то вас постигнет жестокое разочарование. Программа по- прежнему будет выдавать старые сообщения, не обращая на новейшие и замечательнейшие функции никакого внимания - она будет вызывать, вопреки нашим ожиданиям, все ту же функцию people::draw() вместо man::draw() и woman::draw(). Это происходит из-за того, что в нашем примере мы манипулируем указателем на people, вследствии чего все объекты приводятся именно к этому типу (т.е. объекты типа man и woman приводятся к классу people). А т.к. в классе people есть своя реализация функции draw, то вызывается именно она, а не перегруженная в потомках функция draw. На первый взгляд, обойти это досадное недоразумение довольно просто: надо вместо одного цикла написать два - один для man, другой для woman. main{ man* Dima = new man("Dima"); man* Alex = new man("Alex"); woman* Nata = new woman("Nata"); woman* Lena = new woman("Lena"); // создаем связанный список из объектов типа people; Dima -> next = Alex; Nata -> next = Lena; man* m = Dima; while(m->next) { m->draw(); m=m->next } woman* w = Nata; while(w->next) { w->draw(); w=w->next } } Теперь все в порядке. Но постойте, тогда получается сколько типов объектов, столько и циклов - а не многовато ли ?! Да, согласен, многовато, и такое решение явно не подходит. Но что же делать ? Настало время вернуться к понятию объекта и рассмотреть второе замечательное его свойство - виртуальность функций. +---------------+ Каждый объект может содержать свою реализацию | виртуальность | какой-либо функции, наследуемой от предка, а чтобы | функций | вызывалась именно эта реализация, функцию следует +---------------+ объявить виртуальной (вставив в базовый класс перед этой функцией ключевое слово virtual). Перепишем наш пример: class people { people* next; -указывает на подобного себе; char Sex[10]; -пол; char Name[10]; -имя; people(char* aSex, char* aName) -конструктор; {next = 0; strcpy(Sex,aSex); strcpy(Name,aName); } virtual void draw(){cout<----------¬ ¦ next ¦ ¦ ¦(*draw)()¦-->people::draw() ¦ Sex ¦ ¦ ¦---------¦ ¦ Name ¦ ¦ ¦---------¦ ¦поле VMTP-+-- ¦---------¦ ¦ ¦ L---------- L----------- Oбъект VMT man man -----------¬ ->----------¬ ¦ next ¦ ¦ ¦(*draw)()¦-->man::draw() ¦ Sex ¦ ¦ ¦---------¦ ¦ Name ¦ ¦ ¦---------¦ ¦поле VMTP-+-- ¦---------¦ ¦ ¦ L---------- L----------- Oбъект VMT VMT Oбъект woman woman girl girl -----------¬ ->----------¬ ----------¬<¬ ----------¬ ¦ next ¦ ¦ ¦(*draw)()¦->woman::draw()<-¦(*draw)()¦ ¦ ¦ next ¦ ¦ Sex ¦ ¦ ¦---------¦ ¦---------¦ ¦ ¦ Sex ¦ ¦ Name ¦ ¦ ¦---------¦ ¦---------¦ ¦ ¦ Name ¦ ¦поле VMTP-+-- ¦---------¦ ¦---------¦ L-+поле VMTP¦ ¦ ¦ L---------- L---------- ¦ Age ¦ L----------- L---------- (Такая структура объекта создается компилятором фирмы Borland.) Рис 4. Таким образом, при приведении объекта woman к типу people значение в поле VMTP остается прежним и указывает на функцию draw, принадлежащую классу woman, а не people. Из схемы видно, что расположение поля VMTP в базовом классе и его потомках остается постоянным, но содержит разные значения (указатели на свою VMT), даже если ни одна виртуальная функция не была переопределена. Это свойство (разные значения в поле VMTP) можно использовать для определения типа объекта (на этом принципе основана работа функции TypeOf() в Turbo Pascal фирмы Borland - она просто выдает значение поля VMTP). Итак, мы изучили три основных понятия, которые необходимо знать для написания настоящей О.О.Программы: 1) Наследование - свойство классов; 2) Связанные списки - метод работы с объектами; 3) виртуальность функций объекта - свойство классов; Управление программой. ---------------¬ Управление О.О.Программой основано на регистрации ¦ Событийность ¦ и обработке событий. L--------------- В двух словах - это что-то вроде танкиста, который периодически выглядывает из башни танка и посылает сообщения: произошло ли что-нибудь вокруг - если нет, то выдает определенную фразу типа "В Багдаде все спокойно", если да - то что именно произошло (нажатие клавиши на мыши, сигнал от клавиатуры, команда от другого объекта или что-то еще). Далее это сообщение передается всем объектам, участвующим в программе. Так вот, в качестве танкиста, периодически выглядывающего из башни, у нас выступает функция getEvent, а в качестве передатчика и обработчика событий - handleEvent каждого объекта. Отсюда можно понять, что вся программа крутится в следующем цикле: execute() { endState = 0; do{ TEvent e; -структура, содержащая сообщение (событие); getEvent(e); -танкист выглянул и огляделся; handleEvent(e) -все, что увидел - обработал (передал); }while(!endState); -пока не поступит команда - закончить; return endState; -возвращаем причину окончания цикла; } Сначала рассмотрим, что же представляет собой структура TEvent. Этот параметр был полностью позаимствован мной из пакета Turbo Vision фирмы Borland. Это достаточно трудный пакет для освоения О.О.П., но в то же время, если он у Вас есть, то Вам больше ничего не надо (кроме терпения) для освоения О.О.П. Я же постараюсь облегчить Вам труд и сэкономить Ваше время. Здесь часто будут встречаться ссылки на Turbo Vision (TV)( особенно в первой половине книги), т.к. в основу моего пакета положен тот-же TV. Мало того, сначала я расчитывал сделать только графическую надстройку над ним, но увлекшись, решил сделать самостоятельный пакет. Правда, некоторые функции все же перекочевали из TV ко мне - это те функции, которые, на мой взгляд, переделывать не было смысла (т.к. по другому их и не сделаешь); такие места сопровождаются ссылкой на источник. ---------¬ ¦ TEvent ¦ Итак, TEvent - это структура, включающая две переменные L--------- и две функции. typedef unsigned int ushort;//беззнаковое целое; typedef unsigned char uchar;//беззнаковое байтовое; enum Boolean {False, True}; TPoint - класс, содержащий координаты точки (x,y); struct TEvent { ushort what; union { MouseEventType mouse;---->struct MouseEventType события от мышки { uchar buttons; Boolean doubleClick; TPoint where; }; KeyDownEvent keyDown;---->struct KeyDownEvent события от клавиатуры { union { ushort keyCode; CharScanType charScan }; | }; | +------>struct CharScanType { uchar charCode; uchar scanCode; }; MessageEvent message;----->struct MessageEvent сообщение от объекта { ushort command; union { void *infoPtr; long infoLong; ushort infoWord; short infoWord; uchar infoByte; char infoChar; }; }; }; void getMouseEvent(); void getKeyEvent(); }; Поле what - служит для быстрого определения типа события. Сюда заносится его (события) маска. Привожу схему поля what из описания к TV. Биты маски события определены так: ------- Флаги события ---------¬ --T-T-T-T-T-T-T------------------- evMessage = 0xFF00 ¦ ¦ ¦ ¦ ¦ ¦ ¦ ¦ ------------ evKeyboard = 0x0010 ¦ ¦ ¦ ¦ ¦ ¦ ¦ ¦ ¦ ---------- evMouse = 0x000F -+T+T+T+T+T+T+T+T-T-T-T+T+T-T-T-¬ L-+-+-+-+-+-+T+T+-+-+-+T+T+T+T+T- ¦ ¦ ¦ ¦ ¦ ¦ L--- evMouseDown = 0x0001 ¦ ¦ ¦ ¦ ¦ L----- evMouseUp = 0x0002 ¦ ¦ ¦ ¦ L------- evMouseMove = 0x0004 ¦ ¦ ¦ L--------- evMouseAuto = 0x0008 ¦ ¦ L----------- evKeyDown = 0x0010 ¦ L------------------- evCommand = 0x0100 L--------------------- evBroadcast = 0x0200 evNothing = 0x0000; Как видно из рисунка, наложив маску на это поле, можно сразу определить - то ли это событие, которое мы ждем, или его можно отбросить сразу (не проверив конкретного содержания). Маски, характеризующие события от клавиатуры и мышки, будут рассмотрены подробно в главе 3 "Как это делается", посвященной практическому программированию, а сейчас нам более интересны маски, определяющие события от объектов. Да-Да - объекты тоже могут участвовать в этом процессе, "переговариваясь" между собой. evCommand, evBroadcast и evMessage - как раз относятся к этой категории событий. evMessage - указывает (подобно evMouse и evKeyboard) принадлежность события объекту в целом, а вот в чем разница между evCommand и evBroadcast, вам станет понятно ниже. Дело в том, что все события (независимо от кого они поступили - - от внешнего устройства или от объекта) можно разделить на две категории - активные и общие. Активные события - напрвляются только активному, в данный момент, элементу (или элементам). Общие события - посылаются всем элементам по порядку. К активным относятся сообщения, поступающие от клавиатуры. В самом деле, если два объекта находятся на экране (например, две строки ввода), то работаем мы в определенный момент времени только с одним из них, и, следовательно, символы должны поступать именно к нему, а не к его соседу. Такой элемент (т.е. элемент, с которым мы работаем в настоящее время) называется активным или текущим. Так вот, маска evCommand сообщает, что событие должно быть направлено активному элементу. Соответственно, evBroadcast (радиовещательное) определяет, что событие относится к общим (к ним же можно отнести и события от мышки - маска evMouse). Обработка общих событий начинается с "верхнего" элемента в списке, поэтому, если два объекта реагируют на одно и то-же событие, то обработает его только первый получивший (до второго оно дойдет уже в качестве "очищенного"). Сообщения, которыми обмениваются объекты, заносятся в структуру message и представляют собой константы (определенные Вами же). Примером такой константы может служить cmClose (закрыться), посылаемая рамкой окна своему владельцу (т.е. тому же окну), который принимает решение о реакции на эту команду. Если Вы используете пакет, написаный кем-то, то нужно обязательно разобраться со всеми командами, внесенными разработчиками пакета, чтобы, в лучшем случае, не дублировать их в своей надстройке. Вы вправе изменить или дополнить этот набор своими командами (все зависит от ваших потребностей и, конечно, фантазии). Можно придумать такие экзотические команды, как : "Есть ли в этом списке тот, на команду которого я отзываюсь ?" - Вы улыбнетесь - и напрасно - подобное сообщение я использую для определения местоположения окна подменю на экране. В качестве фразы "В Багдаде все спокойно", как Вы догадались, выступает маска evNothing. Теперь рассмотрим второй компонент структуры TEvent - объединение. Первые два элемента (MouseEventType и KeyDownEvent) оставим до лучших времен, а вот третий - message - надо пояснить. Структура message используется для событий, поступающих от объектов. Она включает параметр command и объединение. Первый компонент - ushort command - несет в себе, как уже упоминалось, само сообщение от объекта. Как раз в это поле заносится константа cmClose. Второй компонент - объединение, которое служит для подписи элемента (т.е. обратный адрес - от кого пришло событие). Это еще один полезный параметр для отсеивания "своих" (предназначенных именно для этого объекта) команд. Поясню. Некоторые объекты могут знать - от какого конкретно элемента они могут обрабатывать команды. Например: class Text класс Текст; { содержит в себе void* ScrollBar; указатель на скроллер, от которого может принимать команды; Text(void* aSB) {ScrollBar=aSB;}; void handleEvent(TEvent& event); обработчик событий; } void Text::handleEvent(TEvent& event) { if((event.what == evCommand) если команда от объекта && и (event.message.infoPtr == ScrollBar)) если команда поступила от "моего" скроллера, то . . . обрабатываю ее; clearEvent(event); очищаю событие; } Функция clearEvent(TEvent& event) - очистка (сброс) события - заносит в поле what маску evNothing, а в поле message.infoPtr - адрес объекта, обработавшего его. Вот ее реализация: clearEvent(TEvent& event) { event.what = evNothing; event.message.infoPtr = this; } После такой обработки событие становится "пустым", т.е. объекты не должны обращать на него внимание. Но даже такое событие может послужить информацией для Вас (либо для элементов). Например, можно проверить - обработано ли событие кем-либо (в поле what должно быть evNothing), и если да - то кем именно (прочитать поле message.infoPtr). Осталось проверить - нужный ли элемент получил информацию. Этот прием пригодится как при отладке программы, так и при разработке логики взаимодействия объектов. Например, в моем пакете окно со списком файлов использует это для обновления информации (логика примерно такова: если кто-нибудь из моих "подчиненных" обработал событие, то надо обновить строки вывода). Обратите внимание, что адресный указатель infoPtr можно представить ввиде длинного целого infoLong. Это очень удобно для определения адреса в числовом виде. Мы еще вернемся к TEvent, а сейчас перейдем к функции getEvent(TEvent&) - сборщику событий. void getEvent(TEvent& event) [TV Borland в сокращенном виде] { if( pending.what != evNothing ) если "карман" команд, { поступивших от объекта, не пустой, то event = pending; заполняем event из pending; pending.what = evNothing; очищаем "карман"; } else если "карман" пуст, { event.getMouseEvent(); опрашиваем мышку; if( event.what == evNothing ) если от мыши событий нет, { event.getKeyEvent(); опрашиваем клавиатуру; if( event.what == evNothing ) если событий от клавиатуры нет, idle(); вызываем фоновые операции; } } Или схематично: ---->pending есть ли события от объектов ¦else getEvent---+--->mouse есть ли события от мышки ¦else +--->keyboard есть ли события от клавиатуры ¦else . есть ли события от ... . (далее опрос зависит от Вас - можно . опрашивать последовательный порт, не . занятый мышкой, параллельный или еще . что-нибудь). ¦else если событий ни от кого не поступило, L--->idle() вызываем фоновые операции Переменная TEvent pending - свеобразный "карман", в который можно поместить одно сообщение (команду) от объекта. Т.е. pending выступает в роли устройства, регистрирующего события от объектов (подобно двум другим устройствам, регистрирующих события от мышки и клавиатуры). Заполняется эта переменная функцией putEvent(TEvent&); Вот ее код: putEvent(TEvent& event); { pending = event; } Благодаря такому "карману", и появляется возможность обмена сообщениями между объектами. Со сборщиком событий мы разобрались, рассмотрим обработчик событий handleEvent(TEvent&). Каждый класс имеет свой обработчик событий, который отражает его реакцию на них (на события). Вот типичный код этой функции: void handleEvent(TEvent& event) { switch(event.what) { case evMouseDown: . . . clearEvent(event); break; case evMouseAuto: . . . clearEvent(event); break; case evKeyDown: if (event.keyDown.charScan.charCode == ' ') { . . . clearEvent(event); } else switch (ctrlToArrow(event.keyDown.keyCode)) { case kbUp: . . . clearEvent(event); break; case kbDown: . . . clearEvent(event); break; case kbRight: . . . clearEvent(event); break; } case evBroadcast: if( event.message.command == cmScrollBarClicked && ( event.message.infoPtr == hScrollBar || event.message.infoPtr == vScrollBar ) ) . . . } } Рассмотрим более конкретный пример: class ObjectOne; { virtual void handleEvent(TEvent& event); } class ObjectTwo : public ObjectOne; { virtual void handleEvent(TEvent& event); } void ObjectOne::handleEvent(TEvent& event) { if(event.what ==evKeyDown) switch(event.keyDown.keyCode) { case kbF1: help(); вызов окна подсказки; clearEvent(event); break; case kbF10: menu(); вызов меню объекта; clearEvent(event); break; } } ----------------------------------------первый вариант обработки; void ObjectTwo::handleEvent(TEvent& event) { if(event.what == evKeyDown) if(event.keyDown.keyCode == kbF1) { msg(); вызов окна c сообщением; clearEvent(event); очистили событие; } не передаем событие предку; } Заметьте, что ObjectTwo обработал событие сам, не дав обработать его предку ObjectOne, тем самым полностью подменив его обработчик своим. В результате ObjectTwo не будет реагировать на клавишу F10, в отличие от предка. Такое бывает довольно редко, обычно handleEvent наследника просто дополняет или частично подправляет поведение предка, т.е. из функции handleEvent наследника вызывается handleEvent предка. В зависимости от места вызова обработчика предка можно либо полностью изменить поведение предка на ту или иную команду, либо слегка дополнить. ----------------------------------------второй вариант обработки void ObjectTwo::handleEvent(TEvent& event) { [1] if(event.what ==evKeyDown) if(event.keyDown.keyCode == kbF1) { msg(); вызов окна c сообщением; clearEvent(event); очистили; } ObjectOne::handleEvent(event); передаем событие предку; } Здесь мы полностью изменили реакцию ObjectOne на событие от клавиши F1, очищая его после обработки (до ObjectOne оно не доходит, вернее, он его получает, но уже с пометкой, что его кто-то обработал). В результате на экране появится какое-либо сообщение, а не окно подсказки. Кстати, если бы вызов обработчика ObjectOne был перед кодом обработки ObjectTwo ( место помечено как [1] ), то было бы все наоборот, т.е. сообщение вернется от ObjectOne обработаным. ----------------------------------------третий вариант обработки void ObjectTwo::handleEvent(TEvent& event) { if(event.what ==evKeyDown) if(event.keyDown.keyCode == kbF1) { msg(); вызов окна c сообщением; } не очистили; ObjectOne::handleEvent(event); передаем событие предку; } В третьем варианте мы подправляем обработку события от F1, т.е. ObjectTwo отреагирует, но не очистит его, следовательно, до предка оно дойдет в "нормальном" состоянии и он, в свою очередь, тоже предпримет необходимые ответные действия (появятся сообщение и окно подсказки одновременно). -------------------------------------четвертый вариант обработки void ObjectTwo::handleEvent(TEvent& event) { if(event.what ==evKeyDown) if(event.keyDown.keyCode == kbF2) { save(); сохранить себя в потоке; clearEvent(event); очистили; } ObjectOne::handleEvent(event); передаем событие предку; } В этом обработчике место вызова ObjectOne::handleEvent(TEvent&) вообще не имеет значения, т.к. ObjectTwo не касается "горячих" точек предка, добавляя к ним новую (обработка клавиши F2). Резюме: Итак, мы выяснили, что управление программой представляет собой обработку событий. Обработка событий включает в себя сбор событий (опрос объектов и внешних устройств) и саму обработку поступившей информации. В перерывах между обработкой команд вызываются фоновые операции. Иерархия. Давайте рассмотрим рисунок 1. На нем изображена часть дерева моего пакета Odject miXMax. ------------¬ ¦ GF_Object ¦ L-T---------- +-------TCollection ¦ +-------GF_StrCol ¦ +-------GF_StrCollection ¦ L-------TSortedCollection ¦ +-------TStringCollection ¦ L-------GF_FileColl ¦ ¦ --------------¬ +-----+ GF_GrObject ¦ ¦ L-T------------ ¦ ¦ ¦ ¦ -----------¬ ¦ +-----+ GF_Group ¦ ¦ ¦ L-T--------- ¦ ¦ ¦ ¦ ¦ +-------GF_Window ¦ ¦ ¦ +-------GF_WinPCX ¦ ¦ ¦ +-------GF_WinResize ¦ ¦ ¦ ¦ L------GF_WinTxt ¦ ¦ ¦ L-------GF_WinRest ¦ ¦ ¦ L-------GF_DirWin Рис. 1 Из схемы видно, что все объекты имеют (в конечном итоге) одного родителя - GF_Object. Это один из принципов построения О.О.пакета - все порождать от одного класса. Этот объект может быть чисто абстрактным, экземпляр которого никогда не создается. В нашем же примере этот объект все-таки имеет достаточно полезные функции и переменные. ------------¬ Нужно заметить, что этот объект включает (или обозна- ¦ GF_Object ¦ чает) то необходимое, что будет использоваться L------------ в любом классе. Итак, рассмотрим его "джентельменский набор": class GF_Object { protected: char* ObjectName; public: GF_Object(); ~GF_Object(); char* getName(){return ObjectName;}; void setName(char* name); virtual destroy(GF_Object* o); virtual terminator(){}; } Поле char* ObjectName - указывает на символьную строку, которая идентифицирует класс, т.е. указывает на его имя (как будет показано ниже, это имя можно использовать для определения типа объекта). Как вы уже догадались, функция getName и setName - cоответственно возвращают и устанавливают символьную строку. void GF_Object::setName(char* name) { ObjectName=newStr(name); - строка создается в динамической } памяти, поэтому при уничтожении объекта мы должны не забыть уничтожить и эту строку; GF_Object::~GF_Object() Эта задача возложена на деструктор { объекта; if(ObjectName) delete ObjectName; } Кстати, не забывайте о возможности вставки в деструктор необходимых заключительных операций, но, в то же время, не следует слишком нагружать эту функцию. Я, например, использую деструктор только для уничтожения элементов объекта, за которые он несет ответственность, и о которых другие объекты (и даже пользователь) не догадываются. Теперь обратимся к двум последним функциям destroy(GF_Object*) и terminator(). Эти функции нужны для непосредственного уничтожения объекта (обратите внимание - деструктор мы непосредственно не вызываем, он вызывается автоматически при уничтожении объекта, т.е. как следствие разрушения его). +-- в качестве параметра GF_Object::destroy(GF_Object* o) передается объект извне; { if(o!=0) -если параметр не 0, то { o->terminator() -вызываем терминатор этого объекта; delete o;} -затем удаляем его самого; } Что же это за функция - terminator ? Если говорить кратко - это деструктор, который вызывается принудительно (нами). Обычно он содержит функции, необходимые для успешного удаления объекта. На первый взгляд это просто дублер деструктора - в общем, так оно и есть, но в отличии от него, terminator мы можем вызывать все-таки сами, и он, по своей задумке, более гибок (как мы увидим ниже). В GF_Object - terminator ничего не делает - это пустышка (с дальним прицелом). Сам по себе такой класс не нужен - это просто "фундамент" иерархии. Основную же нагрузку на себе несет другой, более мощный объект - GF_GrObject. --------------¬ ¦ GF_GrObject ¦ В нем закладывается настоящий "джентельменский L-------------- набор", необходимый объектам для их успешной работы и отражающий свойства пакета в целом. В него включаются основные переменные и функции, причем некоторые функции в себе не содержат полезного кода, они просто создаются для будущего использования. Давайте рассмотрим необходимый минимум, который ввиде переменных и функций должен присутствовать почти во всех объектах нашего пакета. class GF_GrObject : public GF_Object { public: GF_GrObject* next; GF_Group* owner; virtual void getEvent(TEvent& event); - сбор событий; virtual void handleEvent(TEvent& event); - обработка событий; virtual void draw(); - рисует себя; //и уже знакомая нам функция: virtual void terminator(); } Поле next - указывает на следующий подобный себе объект. Этот параметр необходим для создания связанных списков и манипуляции объектами в этом списке. Поле owner - указывает на "хозяина" (владельца), в роли которого выступает класс GF_Group (он, кстати, и заполняет это поле), но что это за объект мы рассмотрим ниже (в этой же главе). Теперь рассмотрим две функции - это getEvent(TEvent* event), которая выступает в качестве глаз, ушей и всех остальных органов чувств пакета, и handleEvent(TEvent* event), которая является головой пакета, принимающей решение при обработке событий. В объекте GF_GrObject функция getEvent выглядит так: void GF_GrObject::getEvent(TEvent& event) { if(owner) -если есть у меня хозяин, то owner->getEvent(event); -спрошу у него; } Функция же handleEvent(TEvent& event) - обработчик событий - вообще пустая - она определяется (если это необходимо) в каждом объекте заново. Кратенько рассмотрим функцию draw(). Каждый объект должен уметь рисовать себя на экране, поэтому в каждом из них (или почти в каждом) будет присутствовать своя реализация этой функции. В объекте GF_GrObject она ничего полезного не делает. Draw() просто выставляет флаг, сигнализирующий о том, что объект стал видим: void GF_GRObject::draw() { state|=sfVisible; } Функция terminator() - уже знакома нам. void GF_GRObject::terminator() { hide(); - в данном объекте выполняет обратную задачу функции draw() - cбрасывает флаг sfVisible; } Эти две функции будут вызывать у нас интерес, когда будет рассматриваться объект GF_Group. На самом деле, переменных и функций в таком объекте как GF_GrObject (базовом объекте) намного больше, но они зависят, в основном, от целей создания пакета. Дело в том, что функции, вставленные в первый объект (GF_Object), должны присутствовать во всех классах, а функции во втором объекте (GF_GrObject) уже отражают более конкретные стороны вашей разработки. Например, в нашем случае мы разрабатываем графический интерфейс - поэтому, вероятно, в объекте необходимо поле size, характеризующее его размер на экране. И если вы посмотрите рабочий вариант GF_GrObject (листинг заголовочного файла с комментариями приведен в Главе 4 "Немного о графике") - то найдете это поле. Согласитесь, что это уже более специфичное поле, нежели ObjectName в GF_Object и, следовательно, оно должно присутствовать только в тех классах, которые будут отражены на экране. Например в классе GF_Window, видимо, будет использоваться это поле, а вот в класее TCollection (массив элементов) - оно явно лишнее, так зачем же включать его ?... В Главе 4 "Немного о графике" Вы найдете заголовочный файл, где определен GF_GrObject и сможете увидеть, что же на самом деле представляет из себя настоящий базовый класс. Из класса GF_GrObject нам осталось рассмотреть еще одну переменную - owner - вернее то, на что она указывает - GF_Group. -----------¬ Итак, что же это за объект, который удостоился чести ¦ GF_Group ¦ быть в "джентельменском наборе" любого класса, L----------- порожденного от GF_GrObject ? Kак было сказано выше, взаимоотношения между объектами строятся на основе динамических структур данных (как частный случай был рассмотрен связанный список). Так вот, управлять этими связанными списками и призван т.н. объект-группа. И мы плавно переходим к самому интересному и трудному. От того, поймете ли вы, что такое группа, зависит ваш успех при разработке О.О.программ. Рассмотрим, что должна включать в себя группа: class GF_Group : public GF_GrObject { public: GF_GrObject* last; - первый вставленный; GF_GrObject* current; - текущий (активный); void insert(GF_GrObject*); //вставка объекта в список; GF_GrObject* at(int Index);//нахождение объекта по его //номеру; int indexOf(GF_GrObject*); //нахождение номера объекта; void forEach(void (*func)(GF_GrObject*,void*),void* args); //для каждого в списке - //произвести действия, заданные //функцией: //void func(GF_GrObject*,void*) void Remove(GF_GrObject*); //удалить из списка; // и уже знакомые нам функции, но здесь работающие несколько // по-иному: virtual void eventHandle(TEvent*); virtual void draw(); virtual void destroy(GF_Object*); virtual void terminator(); } Поле last - указывает на первый вставленный в список элемент (перевод с английского -последний, т.к. при обработке событий этот элемент получает события последним из списка). Это отправная точка всех манипуляций со списком. Поле current - указывает на активный элемент. Все активные события направляются именно этому объекту. Функции. insert(GF_GrObject*) - эта функция вставляет элемент в список. Она может быть тривиальна. Вот пример возможной ее реализации: GF_Group::insert(GF_GrObject* o) { o->owner = this; -сразу заполняем поле owner объекта - теперь он имеет хозяина; if(!last) -если список пуст, то поле last { будет указывать на него (на объект); last = o; last->next=last; -"закольцовываем" список, занося в } поле next объекта указатель на себя; else -если же список не пуст, то { o->next = last->next; -"раздвигаем" список; last->next = o; } } Для уяснения этой функции изучите схему, приведенную ниже. -------¬ -->¦ last +--¬ - закольцовка; ¦ L------- ¦ L------------- -------¬ ----¬ -->¦ last +-->¦ 1 +--¬ - раздвигаем; ¦ L------- L---- ¦ L--------------------- -------¬ ----¬ ----¬ -->¦ last +-->¦ 2 +-->¦ 1 +--¬ - раздвигаем; ¦ L------- L---- L---- ¦ L----------------------------- -------¬ ----¬ ----¬ ----¬ -->¦ last +-->¦ 3 +-->¦ 2 +-->¦ 1 +--¬ -pаздвигаем; ¦ L------- L---- L---- L---- ¦ L------------------------------------- и т.д. Рис 1. Теперь можно добавить немного гибкости в функцию insert и позволить пользователю вставлять объект в любое место списка. Для этого добавим следующие строки: if(target!=0) -если есть указание на конкретное место { в списке, то target=prev(target); -находим предыдущий объект и o->next = target->next; -"раздвигаем" список; target->next=o; } И окончательный вариант: GF_Group::insert(GF_GrObject* o, GF_GrObject* target) { o->owner = this; -сразу заполняем поле owner объекта - теперь он имеет хозяина; if(!last) -если список пуст, то поле last { будет указывать на него (на объект); last = o; last->next=last; -"закольцовываем" список, занося в } поле next объекта указатель на себя; else -если же список не пуст, то if(target!=0) -если есть указание на конкретное место { в списке, то target=prev(target); -находим предыдущий объект и o->next = target->next; -"раздвигаем" список; target->next=o; } else //если нет указателя на конкретное место, то { o->next = last->next; -"раздвигаем" список; last->next = o; - вставляем объект перед last; } } В этом дополнении появилась новая, еще не знакомая нам функция prev(GF_GrObject*) - найти предыдущий объект. Дело в том, что у нас получился односвязанный список, т.е. связанный в одном направлении, и элемент видит только одного "соседа" (но ведь их у него двое). По этой причине мы можем двигаться по списку без затруднений только в одном направлении (от указателя к указателю), например, вправо (условно) от объекта. А если нам нужно найти левого соседа, как в предыдущем случае ? Есть два варианта разрешения этой проблемы. Первый - ввести дополнительное поле, указывающее на соседа слева, в результате получим двухсвязанный список. Честно говоря, манипуляции с таким списком проще. Несмотря на дополнительный расход памяти (из-за введения еще одного указателя в объект) мы добиваемся увеличения скорости обработки такого списка. Это становится заметно при большом количестве элементов в списке. Второй - написать функцию, которая вычисляла бы соседа слева (как и сделано у нас). Рассмотрим эту функцию: GF_GrObject* GF_Group::prev(GF_GrObject* target) { if(last) -если список не пустой, { GF_GrObject* res = last; while(res->next!=target) -перебором добираемся до элемента; res=res->next; return res; } else return 0; } Должна ли данная функция присутствовать в нашем "джентельменском наборе" ? - очевидно, ДА. "Но почему же она не была включена ?", - спросите Вы. Этим примером я хотел показать, что все продумать сразу очень тяжело, и подготовить Вас к тому, что первоначально написанный класс будет претерпевать большие изменения, пока не удовлетворит вас полностью. Итак, у нас добавляется еще одна функция prev(GF_GrObject* target). Ну что же, связанный список мы составили. Теперь нам хотелось бы поработать с ним, например определить номер элемента. Для этого существует функция indexOf(GF_GrObject*). Привожу ее в реализации фирмы Borland. int GF_Group::indexOf(GF_GrObject* p) { if(!last) - если список пуст, то и нечего искать; return 0; int index = 0; - начнем с нуля; GF_GrObject* temp = last; - определим точку отсчета; do{ index++; temp=temp->next; } while(temp!=p && temp!=last); ¦ ¦ ¦ L------ пока список не кончится ¦ или L------------------ пока не найдем нужный элемент; if(temp!=0) -если элемент не найден, то return 0; -возвращаем ноль; else return index; -возвращаем индекс элемента; } Рассмотрим функцию, решающую обратную задачу, а именно: нахождение объекта по его номеру: GF_GrObject* GF_Group::at(int index) [TV Borland] { GF_GrObject *temp = last; while( index-- > 0 ) -пока счетчик не ноль; temp = temp->next; -идем по списку; return temp; -возвращаем найденный объект; } Обратите внимание, в предыдущих двух функциях нет проверки на наличие объектов в списке, т.е. мы предполагаем, что last != 0. Так что будьте внимательны ! Эти функции нам очень пригодятся при сохранении объектов в потоке и загрузке их из него. Еще одна функция forEach(void (* func)(GF_GrObject*,void *), void* args ), принимающая в качестве первого аргумента указатель на функцию void func(GF_GrObject*,void *args), и в качестве второго - аргумент args для функции void func(GF_GrObject*,void *args). Принцип работы функции forEach следующий: она проходит последовательно по всем элементам списка и для каждого из них вызывается функция func. void GF_Group:: forEach(void (* func)(GF_GrObject*,void *), void* args ) { GF_GrObject *term = last; GF_GrObject *temp = last; if( temp == 0 ) -не пустой ли список; return; GF_GrObject *next = temp->next;-начинаем с последнего вставленного; do { temp = next; next = temp->next; func( temp, args ); -вызвали функцию, передав ей очередной объект temp из списка и параметр, в качестве которого может быть все, что вашей душе угодно, главное чтобы его правильно распознала функция func (написанная, кстати, Вами же). } while( temp != term );-до конца списка; } Приведу пример и самой функции func: --->void OFFVisible(GF_GrObject* Obj, void*) ¦{ ¦ Obj->hide(); //сбpос флага видимости; ¦} ¦ Теперь все вместе; ¦ ¦void GF_Group::hide() ¦{ ¦ void* zero = 0; ¦ forEach(OFFVisible,zero); ¦} ¦ L------ в данном случае нам этот параметр L-------------- не нужен; Рассмотрим функцию Remove(GF_GrObject* target) - она исключает объект из "списков живых", не уничтожая его. void GF_Group::Remove(GF_GrObject* target) { if(last) -если список не пуст { target->owner = 0; if(target->next==target) -если указывает сам на себя, { следовательно, удаляем последний элемент; last =0; } else -если не последний элемент { GF_GrObject* res = prev(target) -ищем соседа слева; res->next=target->next; -замыкаем список, исключив target; } } Изучите схему -------------- удаляемый V -------¬ ----¬ ----¬ ----¬ -->¦ last +-->¦ 3 +-->¦ 2 +-->¦ 1 +--¬ ¦ L------- L---- L---- L---- ¦ L------------------------------------- -------------- находим предыдущий V -------¬ ----¬ ----¬ ----¬ -->¦ last +-->¦ 3 +-->¦ 2 +-->¦ 1 +--¬ ¦ L------- L---- L---- L---- ¦ L------------------------------------- -------------- поле next переопределяем на элемент V --------¬ номер 1, т.е. -------¬ ----¬ ¦ ----¬ V ----¬ 3->next = 2->next; -->¦ last +-->¦ 3 +-+>¦ 2 +-->¦ 1 ¦--¬ ¦ L------- L---- L---- L---- ¦ L------------------------------------- -------¬ ----¬ ----¬ -->¦ last +-->¦ 3 +-->¦ 1 +--¬ <- итог ¦ L------- L---- L---- ¦ L----------------------------- Рис 2. Теперь объект target cуществует сам по себе, и мы с ним можем делать все что угодно: удалить, вставить в другой список, вставить в другое место того же списка и т.д. Кстати, насчет вставить в другое место того же списка, другими словами, переместить. Неплохая идея для написания еще одной полезной функции - moveTop - переместить наверх списка; void GF_Group::moveTop(GF_GrObject* p) { Remove(p); // комментарии не требуются; insert(p); } Это может пригодиться, например, для перемещения окна наверх стека. Итак, мы рассмотрели основные функции, необходимые для успешной работы со связанными списками, а теперь поговорим об обработке событий. Рассмотрим реализацию функции handleEvent(TEvent&). Ее основная задача с т.з. группы - донести сообщения, содержащиеся в структуре TEvent, до всех членов списка. Как эта задача решается у профессионалов из фирмы Borland ? Во-первых, определяется структура, а вернее целый класс со своим конструктором: struct handleStruct { handLeStruct(TEvent& e, TGroup& g):event(e),grp(g){} TEvent& event; - само событие; TGroup& grp; - подпись того, кто сформировал эту структуру; } Сама функция handleEvent(TEvent&) представляет из себя в упрощенном виде следующее: void handleEvent(TEvent& event) { GF_GrObject::handleEvent(event); -пусть событие обработает сначала предок; handleStruct hs(event, this); -готовим структуру событий; if( (event.what & activEvents) != 0) - если событие активное (activEvent = evKeyboard | evCommand); doHandleEvent(current,&hs); - передаем его в текущий элемент (он единственный из списка обрабатывает это событие); else - если события не активные, forEach(doHandleEvent,&hs); - то всем элементам посылаем это событие, включая и текущий; } Функция doHandleEvent - это фильтр, проверяющий, может ли данный объект обработать событие. static void doHandleEvent(GF_GrObject* p, void *s) { handleStruct* ptr=(handleStruct*)s; -приводим параметр void* s к типу handleStruct; if( (p==0) && -если объект не задан и (p->state & sfActive) == 0) -пассивный (т.е. объект не имеет право обрабатывать события - например, не активная кнопка) return; -то выходим; в противном случае p->handleEvent(ptr->event); -вызываем его обработчик } Такова в упрощенном виде обработка событий группой: ------¬ ¦Group¦ ¦ X ¦ LT-T--- г=============- ¦ ¦ ----------T---+---T---------¬ ¦(А) (Б) (В) (last) г¦=¦==¬ ----+--¬ ---+--¬ ----+--¬ ¦Group¦ ¦Object¦ ¦Group¦ ¦Object¦ LT=T==- L------- L--T--- L------- г=========- ¦ ¦ ¦ ---------+--------¬ +----------¬ ¦ (1) (2) (last) (1) (last) г¦==¦==¬ ----+--¬ ----+--¬ --+----¬ ----+--¬ ¦Object¦ ¦Object¦ ¦Object¦ ¦Object¦ ¦Object¦ L======- L------- L------- L------- L------- ----------- Группа А - первый элемент, получивший событие. Она, в свою очередь, передает событие своим подэлементам (начиная с объекта 1). Когда подэлементы группы А обработают сообщение, оно возвращается в группу X, которая направляет его в следующий элемент ( Б ). И так пока все элементы не просмотрят поступившее событие. Рис 3. Хочу обратить Ваше внимание на использование функции forEach. Она последовательно проходит все объекты, начиная с последнего вставленного. Отсюда вытекает очень важное свойство - объекты не одновременно обрабатывают события, а по очереди. Поэтому у объекта, обрабатывающего событие первым, возникает преимущество перед остальными, т.к. если в списке существуют два или более объектов, реагирующих на одни и те же события, то обработает их сначала последний вставленный, и вполне вероятно, что до остальных оно уже не дойдет. Учтите это. Перейдем к функции destroy(GF_Object*). Основной момент заключается в корректировке списка, т.е. сначала удаляется элемент из списка, а уже потом уничтожается. GF_Group::destroy(GF_Object* o) { Remove(o); delete o; } Неплохо было бы еще сделать проверку на наличие данного элемента в списке, иначе мы рискуем "зависнуть", увязнув в поиске этого элемента. Напрмер: int GF_Group::ValidDestroy(GF_Object* o) { if(!o) - если указатель 0, то и return 0; возвращаем 0; if(indexOf((GF_GrObject*)o)) - если элемент найден, { то destroy(o); уничтожаем его; return 1; возвращаем не ноль; } else - если элемент не найден, return 0; возвращаем 0 } Таким образом, мы рассмотрели уничтожение объекта из списка, а как уничтожается сама группа ? Ответ напрашивается сам собой - надо пройтись по всему списку, последовательно уничтожая элементы. void GF_Group::terminator() { if(last) -если список не пуст, { GF_GrObject* p = last->next; -начинаем с последнего вставленного; last->next=0; -разрываем список; GF_GrObject* p1; do{ p1 = p->next; p->terminator(); -вызываем его terminator(); delete p; -уничтожаем самого; p = p1; }while(p); } } Функция draw(). Объект типа группы виден на экране через свои элементы, т.е. изначально это прозрачный безразмерный класс. Cледовательно, вывод группы на экран заключается в последовательном вызове функций draw() своих элементов. Опуская подробности, можно записать следующий код: void GF_Group::draw() { if (last) - если список не пуст, { GF_GrObject *p = last; - начинаем с первого вставленного; do{ p->draw(); - вызываем его draw(); p=prev(p); } while ( p != last ); if (!current) -если пользователь не определил текущий элемент, setCurrentDefault(); то определяем его сами (первый подходящий, начиная с последнего вставленного); [о текущем элементе current подробней поговорим в следующей главе] } GF_GrObject::draw(); -функция draw() предка; } Как видим, прорисовка идет от первого вставленного, в отличие от обработки событий, где все начинается с последнего вставленного. Обратите на это внимание ! Резюме. 1) Все объекты должны быть порождены от одного. 2) Существуют основные три объекта: а)Базовый объект для всех классов (GF_Object); б)Базовый объект, отражающий цель пакета (GF_GrObject); в)Объект управления, основная задача которого - управление списком (GF_Group); Глава 2. Управление объектами. В предыдущей главе мы узнали, какие свойства класса позволяют разработать необычную логику программы, узнали, что такое управление программой событиями и рассмотрели три базовых класса. Теперь настало время соединить все вместе и рассмотреть работу О.О. пакета. Рассмотрим рисунок 1. getEvent() ------------------------¬ | ------ pending ¦ ¦ | ¦ события от объекта; ¦ GF_Program ¦ <----------+----- getMouseEvent() ¦ ¦ ¦ события от мышки; L------T------T-------T-- L----- getKeyEvent() ¦ ¦ ¦ cобытия от клавиатуры; handleEvent( ) ¦ ¦ ¦ idle( ) ¦ ¦ ¦ draw( ) ¦ ¦ ¦ ¦ V V ----------T-+-------T---------T---->... ¦ ¦ ¦ ¦ V ¦ V V ----+----¬ ¦GF_Panel¦ L---T----- ¦ --------+-T-------T---->... ¦ ¦ ¦ --+-------¬ V V ¦GF_Window¦ L-T-------- --+-------T-------->... ¦ ¦ V V Рис 1. Из схемы видно, что нужно иметь объекту, чтобы стать полноправным членом программы, т.е. подключиться к работе. Это прежде всего "свой" обработчик событий (handleEvent()), который позволяет видеть события и реагировать на них. Благодаря ему объект наделяется "интеллектом". Второе (не обязательный элемент) - иметь функцию фоновых операций (idle()) - тогда в промежутках между обработкой событий объект сможет выполнять какие-нибудь задачи (например, класс GF_Panel подготавливает регенерируемую область экрана). И третье - если объект виден на экране, необходима функция draw(), изображающая его. Эти три функции виртуальные и вызываются в цикле (при определенных ситуациях) V V V ¦ ¦ ¦ handleEvent( ) ¦ ¦ ¦ idle( ) ¦ ¦ ¦ draw( ) ---------T-- handleEvent()----+ ¦ ¦ ¦Object 1+-- idle()-----------+------+ ¦ L--------+-- draw()-----------+------+-------+ ¦ ¦ ¦ ---------T-- handleEvent()----+ ¦ ¦ ¦Object 2+-- idle()-----------+------+ ¦ L--------+-- draw()-----------+------+-------+ ¦ ¦ ¦ ---------T-- handleEvent()----+ ¦ ¦ ¦Object 3+-- idle()-----------+------+ ¦ L--------+-- draw()-----------+------+-------+ ¦ ¦ ¦ и т.д. V V V Рассмотрим как можно распределить задачи в пакете между объектами. Центральной фигурой управления является класс GF_Program, порожденный от GF_Group. Это он собирает события (getEvent()) и передает их вниз путем вызова функций handleEvent() "своих" объектов, которые в свою очередь вызывают обработчики "своих" подчиненных и т.д. Таким же образом происходит вызов фоновых операций (idle()) объектов и вызов функции draw() - вывод объектов на экран. Конструктор класса GF_Program осуществляет необходимые операции по инициализации устройств и глобальных переменных: инициализируется мышка, инициализируется графическая система (загружаем шрифты, устанавливаем тип экрана и т.д.), инициализируется объект GF_Panel, управляющий экраном и т.д. Основная функция у GF_Program execute() (кстати, эта функция имеется в каждом объекте, порожденном от GF_Group). unsigned int endState; unsigned int GF_Group::execute() { endState =0; do { TEvent e; //определяем структуру сбора событий; getEvent(e); //опрашиваем устройства; handleEvent(e); //обрабатываем результат опроса устройств; }while (!endState); return endState; //возвращаем причину выхода из цикла; } Запуск программы осуществляется путем вызова функции run(): void GF_Program::run() { draw(); //прорисовываем объекты; execute(); //запускаем основной цикл; } Как идет обработка событий ? В каждом элементе, порожденном от GF_Group, есть указатель на текущий (активный) элемент - current. Все события делятся на радиовещательные (события посылаются всем объектам в списке по порядку) и активные (события посылаются только одному элементу, на который указывает переменная current). К активным относятся события от клавиатуры и события, помеченные маской evCommand. Такие события посылаются только текущему (current) элементу и больше никому (даже если этот активный элемент не обработал их). (см. Рис 2) ---------¬ ¦GF_Group¦ L---T----- ¦ handleEvent( ) ¦ --------T--L====¬------T------¬ ¦ ¦ (current) ¦ ¦ -+--¬ --+-¬ --¦-¬ --+-¬ --+--¬ ¦ 1 ¦ ¦ 2 ¦ ¦ 3 ¦ ¦ 4 ¦ ¦last¦ L---- L---- L---- L---- L----- Рис 2. Почему возникло такое деление ? Представим две строки ввода. Работаем с одной из них. Если бы события посылались каждому элементу по порядку, то они обрабатывались бы, прежде всего, объектом, стоящим первым в списке, а это не обязательно тот элемент, с которым мы работаем в данное время. Вторая группа событий - радиовещательная. К ним относятся события, помеченные как evBroadcast, а так же и так называемые позиционные (это события от мышки). Для таких событий будут вызываться обработчики всех объектов по порядку, начиная с первого и кончая последним (last), включая и элемент, помеченный как current - текущий. Почему события от мышки мы относим к этой группе ? Рассмотрим обработку позиционных событий. Как только элемент получает такое сообщение, он первым делом определяет положение курсора мыши: если курсор находится на его територии, то событие обрабатывается, если нет - пропускается дальше. Чтобы не возникало конфликтных ситуаций, при которых элемент (А), скрытый элементом (Б), обрабатывает позиционные события, предназначенные не ему (см. Рис. 3), необходимо придерживаться одного --------¬ ¦ А ¦ -------+-------+-------¬ ¦ Б + ¦ ¦ L- mouse ¦ L------T-------T-------- L-------- Рис 3. принципа: первый вставленный элемент в список первым изображается на экране, а т.к. события обрабатываются в обратном порядке, то конфликтов не возникает. Порядок прорисовки элементов: last, 4, 3, 2, 1 (Рис 2). Порядок обработки событий: 1, 2, 3, 4, last (Рис 2). Рассмотрим еще одну интересную ситуацию. Имеются два объекта: строка ввода, помеченная как текущий элемент, и кнопка. Особенность объекта кнопки заключается в том, что он должен просматривать все сигналы от клавиатуры, чтобы не пропустить свой вызов по "горячей" букве. -----¬ ¦Дима¦ Например, кнопка с названием "Дима" реагирует на символ Д ¦- ¦ или на комбинацию Alt-Д. L----- Но вспомним - события от клавиатуры относятся к разряду активных, поэтому они будут посылаться только текущему элементу, т.е. в любом случае попадут в строку ввода, но не в кнопку. Что же делать ? Придется усложнить логику обработки активных событий, введя два дополнительных флага в объект: preProcess и postProcess. Тогда функция обработки событий изменится. void GF_Group::handleEvent( TEvent& event ) { GF_GrObject::handleEvent( event ); handleStruct hs(event, *this); if ( (event.what & forSelectedEvents) != 0) { //если события активные, то phase = phPreProcess; //сначала посылаем активные forEach( doHandleEvent, &hs );//события в элементы с //выставленным флагом preProcess; phase = phSelected; //далее посылаем события doHandleEvent( current, &hs ); // в текущий элемент; phase = phPostProcess; //напоследок посылаем событие forEach( doHandleEvent, &hs );//в элементы с выставленным } //флагом postProcess; else //дp. события (события от мышки и помеченные evBroadcast) { forEach( doHandleEvent, &hs); } } ---------¬ ¦GF_Group¦ L---T----- ¦ handleEvent( ) ¦ --------T------T------T-----+-------T------T------T------¬ ¦ ¦ (current) ¦ ¦ ¦ ¦ ¦ ¦ -+--¬ --+-¬ --+-¬ --+-¬ -+--¬ --+-¬ --+-¬ --+-¬ --+--¬ ¦ 1 ¦ ¦ 2 ¦ ¦ 3 ¦ ¦ 4 ¦ ¦ 5 ¦ ¦ 6 ¦ ¦ 7 ¦ ¦ 8 ¦ ¦last¦ L---- L---- L---- L---- L-T-- L-T-- L-T-- L-T-- L----- postProcess ----- ¦ ¦ L-preProcess postProcess ------------ L--------preProcess Рис 4. Теперь активные события сначала попадут элементам 7 и 8, далее элементу 3 и в заключение элементам 5 и 6 (Рис. 4). Так что же получается в нашем примере ? В кнопке выставляем флаги preProcess и postProcess. Когда событие поступило в кнопку, смотрим, какой группе элементов оно адресовано: если элементам с выставленным флагом preProcess, то проверяем комбинацию Alt-Д, если элементам с флагом postProcess, то проверяем символ. В нашем случае кнопка будет откликаться только на комбинацию Alt-Д (не считая реакцию на мышку), т.к. простые символы не будут пропускаться строкой ввода (она их сама обрабатывает как текущий элемент). Честно говоря, эти флаги и были позаимствованы мной из TV в первую очередь только из-за кнопок (по крайней мере я их использую в основном в них). Элемент, помеченный как current, желательно выделять на экране, например, обводить рамкой или изменять цвет. Но заметим, что не каждый элемент может быть активным. Например, рамке окна незачем быть таковой. Поэтому в объект необходимо ввести еще один флаг, показывающий, может ли элемент быть выбранным (активным) - ofSelectable. По умолчанию активным элементом считается последний вставленный объект (при условии, что он может быть таковым, в противном случае перебираем весь список до тех пор, пока не встретим объект, удовлетворящий нашему условию). Как видите, события доводятся до объектов в строгом порядке. Но что делать, если нам необходимо нарушить его и "привлечь внимание" элемента именно в данный момент, а не дожидаться, пока событие дойдет до него обычным путем (т.е. избежать постановки события в очередь pending), причем нет никакой гарантии, что его не перехватит другой элемент. Например, при скроллинге текста, нам необходимо все время взаимодействовать с полосой скроллинга, чтобы она правильно и своевременно отражала положение ползунка. И наоборот, при перемещении ползунка мышкой необходимо сообщить об этом текстовому элементу, к которому она (полоса скроллинга) присоединена, для своевременного обновления текста. Для таких действий может служить небольшая функция message. void *message( GF_GrObject *receiver,//кому послание; unsigned what,//к какому разряду относится сообщение; unsigned command,//конкретная команда; void *infoPtr //кто послал; ) { if( receiver == 0 )//если места назначения нет, return 0; //выход; TEvent event;//создаем структуру событий; event.what = what; //заполняем поля; event.message.command = command; event.message.infoPtr = infoPtr;//подпись; receiver->handleEvent( event ); /*посылаем объекту, т.е. вызываем его обработчик событий и передаем ему заполненую структуру TEvent*/ if( event.what == evNothing )//если объект обработал событие, return event.message.infoPtr;//возвращаем его подпись; else//если объект не обработал событие, return 0;//возвращаем 0; }/* END of Function message */ Приведу пример одного из вариантов использования данной функции. У класса GF_TxtDraw, который выводит текст на экран, есть два указателя: GF_ScrollBarV* ScrollV;//указатель на вертикальную полосу скроллинга; GF_ScrollBarH* ScrollH;//указатель на горизонтальную полосу скроллинга; Рассмотрим обработку объектом GF_TxtDraw события "нажата клавиша 'стрелка вниз'": void GF_TxtDraw::handleEvent(TEvent& event) { switch(event.what) { case evKeyDown: switch(event.keyDown.keyCode) { case kbUp: . . . clearEvent( event ); break; case kbDown://если нажата клавиша "стрелка вниз"; TMouse::smarthide(makeGlRect());//скрываем мышку; Dn(); //вызываем функцию скроллинга окна вверх; echoMail = calcPurcent(0);/*подсчитываем процент, на который изменилось положение текста;*/ if(ScrollerV)//если вертикальный скроллер прикреплен, то message(ScrollerV,evBroadcast,sbChange,this);/*сообщаем ему об изменении положения вывода текста;*/ TMouse::show();//включаем мышку; clearEvent( event );//очищаем событие; break;//выход; case kbPgUp: . . . } } } Два пути разработки классов. Концепция соединения функций и данных в одном объекте располагает к созданию очень мощных по "интеллекту" объектов и больших по размеру кода классов. Написание класса напоминает создание снежного кома (снежную бабу все когда-то лепили): когда класс готов, выясняется, что чего-то не хватает, и Вы добавляете и добавляете новые данные и функции. Поэтому очень важно научиться "останавливаться". Этот навык приходит с опытом создания О.О.Программ, так что поначалу у Вас будут огромнейшие классы. Есть два направления, по которым можно прорабатывать этот вопрос. Первый путь - это грамотная иерархия. Например: -----------¬ ¦StaticLine¦ - выводит строку в определенную область на экране, L----T------ отсекая "лишние" символы; V ------------¬ - в отличии от предка, отсечка строки происходит ¦DynamicLine¦ только при выводе на экран, поэтому с помощью L----T------- стрелок <- -> можно просматривать всю строку; V ------------¬ ¦ InputLine ¦ - добавлена функция, позволяющая вводить символы; L------------ (все три класса можете увидеть в демонстрационной программе) Построение такой иерархии не самоцель. Необходимо пытаться, чтобы каждый класс в иерархии был законченным и его можно было использовать в программе как самостоятельный элемент. Приведенный выше пример хорошо иллюстрирует это. StaticLine используется мной при выводе числовых показаний индикаторов. Количество выводимых символов определяется необходимой точностью, требуемой заказчиком (количеством знаков после запятой). DynamicLine применяется при выводе сообщений в ограниченном пространстве. Возможность скроллинга позволяет не расчитывать место, занимаемое всей строкой, и соответственно не подлаживаться под разную длину сообщений. InputLine основное предназначение - это введение данных в программу. Все вышесказанное, правда, не исключает создания промежуточных (базовых) классов, которые напрямую в программе не применяются. Примером может служить иерархия скроллинговых полос: ---------¬ ¦Scroller¦ <- базовый класс L---T----- ----------+---------¬ -----+-----¬ ------+----¬ вертикальный -> ¦ScrollBarV¦ ¦ScrollBarH¦ <- горизонтальный скроллер L----------- L----------- скроллер Основной особенностью базового класса является абстрактность. В него закладываются основные переменные, которые свое фактическое значение получают в потомках. Функции такого класса в большинстве своем виртуальные и как правило абстрактные. Полезный код таких функций появляется только у потомков. Второй путь -это распределение функций между несколькими классами. Примером могут служить классы GF_Panel и GF_Window. GF_Panel - это менеджер экрана. Он отвечает за расположение и уничтожение объектов на экране. Основные задачи - сохранение образа объекта (в файле или памяти) и восстановление изображения под объектом после его уничтожения. Некоторые особенности регенерации экрана (например, элемент по координате X должен выводиться на границе байта) возлагают на объект, которым непосредственно манипулирует GF_Panel (например, GF_Window), ряд задач. В частности, GF_Window сам корректирует свои начальные координаты (подгоняя их по горизонтали к границе байта) и свой размер ( ширина окна должна быть кратна 8, т.е. занимать целое число байт). Первоначально класса GF_Panel у меня не было и все классы (в частности GF_Window) напоминали эдаких акселератиков, которые помимо своих прямых обязанностей могли выполнять еще и кучу задач, прямого отношения к ним не имевших (хотя бы ту же задачу регенерации экрана). Несмотря на такой "ум", между ними на экране периодически возникали конфликты, которые решались путем снежного кома (добавлением новых функций, переменных, флагов и т.д.). В конце концов я пришел к выводу, что нужны классы-менеджеры (все они порождаются от класса GF_Group). GF_Panel и является представителем такого класса. В качестве одного из объектов, которыми манипулирует GF_Panel, выступает окно GF_Window. Это окно - графическое представление класса GF_Group. Модальность. -------------¬ ¦Модальность.¦ Любой объект, порожденный от GF_Group, в процессе L------------- выполнения программы может стать модальным. В этом случае события поступают только в модальный элемент и больше никуда. Вот как это делается: GF_Window* Win = new GF_Window(....); // создали в динамической памяти экземпляр класса GF_Window; GF_Program::ScreenPanel->insertView(Win); // вставили объект в панель экрана и изобразили его на экране; Win->execute(); // вызвали функцию execute() этого объекта; Функция execute() присутсвует во всех классах, порожденных от GF_Group. После вызова этой функции все события направляются только в объект, которому она принадлежит (следовательно, вся программа замыкается на этом элементе). Объект делается модальным в тех случаях, когда требуется непосредственное вмешательство пользователя, от решения которого зависит дальнейший ход программы. Как правило, модальными являются диалоговые окна, содержащие ряд вопросов или установок, влияющих на программу. Примером модального объекта может служить запрос: "Завершить программу ? Да/Нет". Модальность не означает остановки всей программы - она функционирует по-прежнему. Просто цикл обработки событий начинается и заканчивается в одном элементе. Но что самое интересное - это то, что фоновые операции объектов, НЕ ВХОДЯЩИХ В МОДАЛЬНУЮ группу, ВЫЗЫВАЮТСЯ по-прежнему ! Фоновые операции. -----------------¬ ¦Фоновые операции¦ Это может Вас удивить, но давайте еще раз ¦ idle() ¦ вспомним функцию сбора событий L----------------- getEvent(TEvent& event) класса GF_GrObject: GF_GrObject::getEvent(TEvent& event) { if(owner) // если существует владелец объекта, то owner->getEvent(event); //спрошу у него; } Таким образом мы добираемся до самого главного объекта в программе (у нас это GF_Program), который опрашивает устройства (т.е. собирает события). ------ pending ---------------¬ ¦ события от объекта; GF_Program:: ¦ getEvent() ¦<---------+----- getMouseEvent() L--------------- ¦ события от мышки; ^ L----- getKeyEvent() ¦ cобытия от клавиатуры; --------+------¬ GF_Panel:: ¦ getEvent() ¦ L--------------- ^ --------+------¬ GF_Window:: ¦ getEvent() ¦ L--------------- Это одно из чудеснейших свойств программы данной структуры. Благодаря такой возможности можно добиться сногсшибательных эффектов. Например, в демонстрационной программе попробуйте мышкой переместить окно (для этого необходимо мышку подвести к заголовку окна, нажать левую клавишу и не отпуская ее, перемещать мышку). Обратите внимание, что при этой операции фон продолжает работать (и это без трюков с прерываниями от таймера !). Следовательно, на фоновые операции можно возлагать довольно ответственные операции (не критичных ко времени), например, проверка памяти. Не забывайте: фоновые операции не должны быть слишком длинные по времени. Если Вам необходимо вставить большой цикл (много проходов), то либо измените код, исключив такой цикл, либо разбейте его на несколько маленьких циклов и выполняйте по одному циклу за один вызов фоновой функции. Для примера посмотрите как организована функция idle() класса GF_Panel в разделе "Работа с экраном". Такое свойство фоновых функций (периодически вызываться) порождает другую проблему. Может возникнуть ситуация, когда не желателен вызов фона (например, при критических ситуациях, решение которых возлагается на пользователя). Для запрета фона можно ввести флаг разрешения/запрещения вызова фоновых операций. Причем, если флаг будет "свой" для каждого объекта, то можно получить дополнительную гибкость в управлении фона каждого экземпляра в отдельности (т.е. можно запрещать/разрешать вызов отдельных фоновых операций). Глава 4. Как это делается. Сохранение объектов в потоке и загрузка объектов из потока. Использование потоков в качестве простой замены функций printf, write, read и т.д. напоминает езду по скоростной дороге на самосвале. При таком применении кроме увеличения кода мы ничего не получим. Но тогда зачем же нужны потоки ? Главное их предназначение - сохранение и загрузка объектов. Посмотрите: int I = 1; ofstream os; os << I; этот код можно с успехом заменить на int I = 1; putw(I,...); А вот следующим строчкам ofstream os; man* Dima = new man("Dima"); os << Dima; аналогичной замены не найдете. Здесь сохраняем не встроенный тип, а свой собственный. Для успешного освоения материала вспомните о возможности перегрузки операций. Перегрузка операций - это ассоциация стандартной операции с определенным объектом. Например: ofstream& operator << (ofstream& os, man* m) { оператор << принимает два параметра: ссылку на поток и указатель на объект (типа man) os.writeString(man->Sex); -сохраняем пол os.writeString(man->Name); -сохраняем имя return os; возвращаем ссылку на поток } Я не буду вдаваться в описание механизма перегрузки (это можно прочитать в любом букваре по C++), а перейду сразу к делу. Итак, сохранение и загрузка объектов. Сначала вспомним составные части любого класса - их две: 1) данные и 2) функции. Следовательно, для сохранения (загрузки) объекта необходимо вывести в поток его данные и функции. Как записать данные более или менее понятно, а как записать функции ? Рассмотрим процесс инициализации объекта. class object { int I; - к.-л. поле object(){}; ~object(){}; setI(int i){i=I;}; - инициализация поля I } main() { object Object; - теперь у нас есть экземпляр объекта Object, } в котором присутствует поле I (пока не инициализированное) и вполне работоспособная функция setI. Стоп... Значит, функции создаются "автоматически" и, следовательно, сохранять (а соответственно и загружать) их не надо. Тогда процесс записи класса сводится к сохранению его полей, а загрузка - к инициализации объекта и считыванию полей. Т.е. наша задача выглядит так: Запись класса. 1) Определить ТИП объекта. По ТИПу найти его "паспорт" (заполненый ранее). 2) Из паспорта "прочитать" символьную строку - ИМЯ объекта. Записать ее в поток. 3) Записать в поток данные объекта. паспорт ------¬ ---->¦ тип ¦ объект (1) ¦ имя-+---(2)---¬ -------¬ ¦ ¦build¦ ¦ ¦ тип--+---- L------ ¦ ¦данные+----------(3)---------¬ ¦ L------- V V ======T====== ДИСК Рис 1. Загрузка класса. 1) Считать ИМЯ класса. 2) По имени найти его паспорт. 3) Вызвать конструктор объекта - поле build (создаем экземпляр). 4) Считать данные объекта. по имени нашли паспорт паспорт ------¬ | ¦ тип ¦ создали экземпляр объекта ---(2)-------->¦ имя ¦ | ---------¬ ¦ ¦build+----(3)------>¦ объект ¦ ¦ L------ L--------- (1) - считали имя ^ ¦ ¦ ¦ ----------------(4)------------ ¦ ¦ | ¦ ¦ Считали данные объекта ^ ^ ДИСК ======T====== Рис 2. Из схем видно, что для каждого объекта необходима структура (паспорт), где содержатся: 1 - ТИП; 2 - ИМЯ; 3 - указатель на функцию, создающую экземпляр объекта; Рассмотрим эти три поля. ------¬ 1) ТИП. ¦ ТИП ¦ Что такое тип мы выяснили в первой главе. L------ А вот как его определить ? Тип объекта определяется по полю, которое содержит указатель на VMT (таблицу виртуальных правил). Эта таблица создается автоматически при наличии либо конструктора, либо виртуальных функций, и для каждого класса она своя (см. Рис 4 в главе 1). Следовательно, по содержанию в данном поле все классы можно разделить на типы (типы объектов совпадают, если совпадают их значения в этом поле). Но как узнать местоположение поля VMT в объекте ? Для этого пойдем на маленькую хитрость. Т.к. интересующее нас поле создается компилятором автоматически при первой встрече с конструктором или виртуальной функцией, и его местоположение от родителя к потомкам НЕ изменяется (см. Рис 4 в главе 1), то можно создать абстрактный класс, где сами зададим расположение этого поля. Все объекты, в дальнейшем, будем порождать от него. class TStreamable { здесь нет данных, а только виртуальная функция, virtual Zero() = 0; следовательно, поле VMT будет располагаться в } этом классе со смещением ноль TStreamable VMT ----------¬ ----------¬ ¦поле VMTP+---->¦(*Zero)()¦->TStreamable::Zero() ¦---------¦ L---------- L---------- Теперь определим макро, которое будет указывать на значение в поле VMTP, а следовательно, на тип объекта: #define __DELTA(d) (FP_OFF( (TStreamable*)(d*)1 )-1) <-это макро справедливо только для классов, созданных компилятором фирмы Borland. Вот и все ! ------¬ 2) ИМЯ. ¦ ИМЯ ¦ Это поле необходимо для нахождения паспорта объекта при его L------ загрузке извне ( например, с диска ). В качестве имени выступает символьная константа, которая закрепляется за каждым объектом. Например #define cpMan "\x1A\x1B\x1C" . --------¬ 2) build. ¦ build ¦ Это указатель на функцию, которая создает объект, L-------- др. словами это указатель на конструктор класса. Посмотрите еще раз на схемы. Такая структура загрузки и сохранения реализована в TV фирмы Borland. Но если разобраться, то можно заметить - из ТРЕХ полей в паспорте ДВА ДУБЛИРУЮТ друг друга, т.е. одно из них ЛИШНЕЕ. Но какое ? Ответ для Вас, видимо, будет неожиданным - это ТИП объекта ! Ну как, круто ? Мы столько времени потратили для разработки методики его определения, а оказывается, что это поле в паспорте не нужно. Давайте разберемся. Конечно, сам по себе тип необходим, но не в таком виде. Подумайте: для идентификации объекта при загрузке из потока используется символьная строка - его имя - и справедливо, ведь это константа, не зависящая от условий работы программы. А что такое тип - это вполне конкретный физический АДРЕС таблицы VMT. Но сегодня он один, а завтра другой, да что там завтра, загрузив резидентную примочку, мы уже изменяем физический адрес начала нашей программы в памяти, а, следовательно, и указатель на VMT класса. Так зачем же нам тащить этот параметр - определить тип можно и по символьной строке, другими словами, распознать тип. В качестве символьной строки будем использовать введенное ранее в класс GF_Object поле ObjectName. Вот окончательный вариант базового класса: typedef GF_Object* (*PFObj)(); - указатель на функцию, возвращающую указатель на GF_Object, т.е создающую экземпляр типа; class GF_Object { protected: char* ObjectName; - значение этого поля заносится в паспорт в графу ИМЯ; PFObj BuildFunc; - значение этого поля заносится в паспорт в графу build; public: GF_Object() { ObjectName = 0; BuildFunc = 0; }; virtual ~GF_Object(); virtual char* getName() { return ObjectName; };- возвращаем имя объекта (распознаем класс); virtual PFObj getBuildFunc() { return BuildFunc; };-возвращаем указатель на конструктор (для поля build); void setName(char* name); -заносим имя объекта; void setBuildFunc(PFObj f) { BuildFunc = f; }; -заносим указатель на конструктор; virtual void write(gf_wstream& os) {}; -запись объекта в поток; virtual void* read(gf_rstream& is) {return this;};- извлечение объекта из потока; и знакомые нам функции virtual void destroy( GF_Object * ); -уничтожение объекта; virtual void terminator(); -терминатор объекта; }; Теперь запись класса в поток будет несколько изменена: Запись класса. 1) Зарегистрировать объект (создать его паспорт при необходимости); 2)Записать в поток имя; 2) Записать в поток данные объекта; паспорт (1) ------¬ +---->¦ имя +---(2)---¬ объект | ¦build¦ ¦ -------¬ | L------ ¦ ¦данные+----------(3)---------¬ ¦ L------- V V ======T====== ДИСК Рис 3. Схема считывания класса осталась та же (только в поспорте пропала запись ТИП). struct PassportObject - паспорт класса { char* NObj; //имя объекта; PFObj Build; //указатель на функцию, создающую экземпляр объекта; }; А где хранится паспорт объекта ? Рассмотренная структура сохранения и загрузки объекта подразумевает создание и использование базы данных, в которой содержатся паспорта классов, участвующих в программе. Эта база представляет собой коллекцию (класс "коллекция" рассмотрен в разделе "Коллекции"). Такая база является единовременной и создается каждый раз заново либо при старте программы (в специальном модуле), либо в процессе сохранения объекта в поток (перед его сохранением заносятся необходимые данные в паспорт, а паспорт в базу данных). Эта база может храниться как в памяти машины, так и на внешнем носителе (на диске). Основным недостатком такой базы является ее нестатичность, т.е. при новом запуске программы база должна создаваться снова (в случае хранения ее на диске - полностью обновляться). Этот недостаток объясняется наличием в паспорте переменной build - указатель на конструктор, где хранится ФИЗИЧЕСКИЙ адрес конструктора класса. Рассмотрим конкретный пример класса, способного записываться и загружаться. class GF_Frame : public GF_GrObject - рамка окна; { public: char* Title //заголовок; TRect RectBtMove; //область кнопки "пеpемещения"; TRect RectBtClose; //область кнопки "закpытия"; int clrFrame; //цвет обода; int clrFill; //цвет поля; GF_Frame(TRect rect, char* title); ~GF_Frame(); . . . virtual void write(gf_wstream& os); //запись себя в поток; virtual void* read(gf_rstream& is); //извлечение себя из потока; Эти функции вызываются при сохранении и извлечени объекта. Т.к. они виртуальные, то у каждого класса должен быть определен свой набор этих функций. Вы ответственны за это. Структура их проста: сначала вызывается аналогичная функция предка, а затем сохраняются (загружаются) поля самого объекта. static GF_Object* build(); /*функция, создающая экземпляр объекта (на нее указывает поле build в паспорте);*/ GF_Frame(Building); /*конструктор для работы с потоком, где enum Building {buildInit} формальная величина, идентифицирующая конструктор;*/ };/* END of Class GF_Frame */ Реализация функций: //...............................................конструктор GF_Frame::GF_Frame(TRect rect, char* title): GF_GrObject(rect), инициализация полей Title(newStr(title)), clrFrame(LIGHTGRAY), clrFill(LIGHTGRAY) { setName("GF_Frame"); инициализируем свое имя; setBuildFunc(build); инициализируем указатель на функцию, создающую экземпляр объекта; } //....................................запись объекта в поток void GF_Frame::write(gf_wstream& os) { GF_GrObject::write(os); сначала даем записать себя предку; os.writeString(Title); записываем свои поля; os << clrFrame << clrFill; } //..............................считывание объекта из потока void* GF_Frame::read(gf_rstream& is) { GF_GrObject::read(is); сначала считывает себя предок; Title = is.readString(); считываем свои поля is >> clrFrame >> clrFill; return this; } //..............................функция, создающая экземпляр объекта GF_Object* GF_Frame::build() <-----------------------------------¬ { ¦ return new GF_Frame(buildInit); вызываем конструктор; ¦ } ¦ ¦ --------------------------------- ¦ //................¦.конструктор для создания экземпляра класса ¦ V (используется при загрузке объекта из потока) ¦ GF_Frame::GF_Frame(Building) : GF_GrObject(buildInit) ¦ { делаем только самые необходимые операции ¦ setName("GF_Frame"); - заносим имя; ¦ setBuildFunc(build); - заносим указатель на --------------------- Title = 0; } Осталось перегрузить операций << и >>: inline gf_rstream& operator >> (gf_rstream& io, GF_Frame& obj) { return io >> (GF_Object& )obj;} inline gf_rstream& operator >> (gf_rstream& io, GF_Frame* &obj) { return io >> (GF_Object*&)obj;} inline gf_wstream& operator << (gf_wstream& io, GF_Frame& obj) { return io << (GF_Object&)obj;} inline gf_wstream& operator << (gf_wstream& io, GF_Frame* obj) { return io << (GF_Object*)obj;} В результате перегрузки получаем ассоциацию объекта типа gf_wstream и gf_rstream. с объектом типа GF_Object, а т.к. все классы в конечном итоге имеют предка GF_Object, следовательно, все они подпадают под эту перегрузку. Теперь можем написать gf_wstream os("File"); GF_Frame frame(TRect(10,10,20,20),"Окно"); os << frame; объект будет выведен в файл "File" Классы gf_wstream и gf_rstream - это замена стандартных потоков ввода/вывода (ifstream и ofstream). Честно говоря, можно было и не заменять их, а просто перегрузить << и >> соответсвенно для ifstream и ofstream. Но независимо от того, сделали Вы замену или нет, структура функций сохранения и загрузки останется типичной. Рассмотрим реализацию этих функций. Перегрузка операции <<. gf_wstream& operator << ( GF_Object& obj ); - ассоциация с ссылкой на тип GF_Object; gf_wstream& operator << ( GF_Object* obj ); - ассоциация с указателем на тип GF_Object; //............................................................... gf_wstream& gf_wstream::operator << ( GF_Object* obj ) { запись объекта типа GF_Object в поток; GF_Program::ObjCollection->checkRegistration(obj); - проверка наличия паспорта объекта в массиве (в случае необходимости создаем его и заполняем); writeByte('*'); - запись метки, сигнализирующей о начале описания объекта; writeString(obj->getName()); - запись имени-типа объекта; obj->write(*this); - вызываем функцию записи объекта (эта функция у каждого класса своя); return *this; } //............................................................... gf_wstream& gf_wstream::operator << ( GF_Object& obj ) { эта функция аналогична предыдущей; GF_Program::ObjCollection->checkRegistration(&obj); writeByte('*'); writeString(obj.getName()); obj.write(*this); return *this; } Перегрузка операции >>. GF_Object* gf_rstream::readObj(GF_Object* obj) { базовая функция считывания объекта из потока char ch = readByte(); if (ch != '*') { MsgBox("> !!! <\nНЕ найденo начало объекта !\ \nПpовеpьте файл.\n\Пpогpамма аваpийно завеpшена !",0,12); exit(1); } if (!obj) //если объект не создан, создаем его; { obj = GF_Program::ObjCollection->buildObj(readString()); if (!obj) //если попытка неудачна, exit(1); //выходим из программы; } obj->read(*this); //заполняем поля объекта; return obj; //возвращаем указатель на объект } //............................................................... gf_rstream& gf_rstream::operator >> ( GF_Object &obj ) {загрузка объекта из потока по ссылке, т.е. экземпляр объекта уже создан readObj(&obj);//передаем указатель на экземпляр объекта return *this; } //............................................................... gf_rstream& gf_rstream::operator >> ( GF_Object* &obj ) {загрузка объекта из потока (экземпляр объекта еще не создан) obj = readObj(0); return *this; } Благодаря такому свойству объектов (умение сохранять и загружать себя) программа приобретает вид, подобный интерпретатору: она манипулирует тем, что ей дают. Поэтому можно инициализацию объектов вынести за пределы исполняемого кода. Таким образом (по утверждению фирмы Borland) можно сократить исполняемый код на 10%. Но, что самое интересное - можно перестраивать программу не переделывая основной код, только заменяя файл ресурсов (где хранятся данные для объектов). Поэтому действующая программа может за несколько секунд превратиться в демонстрационную путем замены этого файла ресурсов. Также возникает возможность прервать выполнение программы практически в любой точке и сохранить ее состояние (т.е. объекты) в файле ресурсов. При возобнавлении работы необходимо загрузить ранее сохраненные объекты, и Вы снова прдолжаете работать. В демонстрационной программе, прилагаемой к данной рукописи, попробуйте сохранить и загрузить текущее состояние панели экрана. Для этого необходимо войти в пункт меню "Pесурс". После сохранения панели посмотрите на размер файла "DIMA.RES", где содержатся объекты, присутствующие на экране. Этот файл, вопреки ожиданиям, очень маленький. Кстати, режим "Авто" - это просто загрузка заранее подготовленного файла ресурсов. Его размер приблизительно равен 15 Kb, хотя он включает в себя информацию почти обо всех объектах данного пакета. Коллекции. Наряду со связанными списками коллекции являются еще одним представителем динамических баз данных. Коллекция - это массив указателей на указатели. Этот массив может изменять (увеличивать) свой размер, если количество элементов превышает первоначально заданное число. Все это делает коллекции очень привлекательными для программиста. Благодаря тому, что это массив указателей типа void* (для паскалистов поясню void* - указатель (адрес) на элемент неопределенного типа), в него можно вставлять любые объекты (поэтому в одной коллекции могут находиться экземпляры разных классов - это еще одна привлекательная сторона коллекций). Рассмотрим подробней коллекции (основной код взят из пакета TV фирмы Borland [на мой взгляд очень удачный]). Внутренние переменные коллекции: void **items;//указатель на начало массива; int count; //количество элементов в массиве; int limit; //предельное число элементов в массиве; int delta; //значение, на которое увеличивается массив; Boolean shouldDelete;//переменная, указывающая, нужно ли разрушать элементы при уничтожении массива; //конструктор коллекции TCollection::TCollection( int aLimit, int aDelta ): /* первоначальный размер --+ +- приращение, на которое увеличивается список при достижении limit (предела); */ count( 0 ), // - текущее количество элементов в коллекции; items( 0 ),// указатель на первое значение (т.е. на сам массив); limit( 0 ),// предельный размер массива; delta( aDelta ),// приращение; shouldDelete( True ) //должны ли уничтожаться элементы при // уничтожении массива; { setLimit( aLimit ); //установка размера массива; setName("TCollection"); setBuildFunc(build); } TCollection::~TCollection() { freeAll(); // уничтожение всех объектов из массива; setLimit(0); } void TCollection::terminator() { if( shouldDelete ) //если уничтожение членов массива разрешено, freeAll(); //то удаляем их; setLimit(0); } void *TCollection::at( int index )//нахождение элемента по его { номеру; return items[index];//индексируем область, занимаемую массивом, } //и извлекаем необходимый элемент; void TCollection::atRemove( int index )//удаление указателя на {// элемент по его номеру (при этом сам ЭЛЕМЕНТ НЕ УНИЧТОЖАЕТСЯ !) if( index >= count )//если номер элемента неправильный, return; //error(1,0); вызываем функцию ошибки; count--; //уменьшаем счетчик; memmove( &items[index],&items[index+1],(count-index)*sizeof(void * ); /*"сжимаем" область массива, последовательно перемещая указатели: --------¬ -------¬-------¬ V ¦ V ¦V ¦ --------T-------T-------T--+----T---+---T---+---¬ ¦ item0 ¦ item1 ¦ item2 ¦ item3 ¦ item4 ¦ item5 ¦ L-------+-------+--T----+-------+-------+-------- L- удаляемый элемент Обратите внимание на эту манипуляцию с памятью. Этот прием можно эффективно использовать во многих ситуациях. Применение функции memmove гарантирует корректную пересылку данных (она сначала определяет направление пересылаемых данных, а затем принимает решение, каким образом пересылать данные, чтобы не запортить их [смотрите исходные коды библиотек фирмы Borland]). */ } void TCollection::atFree( int index ) { //удаление элемента из списка с одновременным его уничтожением; void *item = at( index );//находим сам элемент; atRemove( index ); //исключаем его указатель из списка; freeItem( item ); //уничтожаем его; } void TCollection::atInsert(int index, void *item) {//вставить элемент в массив в место, заданное параметром index; if( index < 0 )//если индекс неправильный, return;//error(1,0); то выход; if( count == limit ) //если позиция равна пределу массива, то setLimit(count + delta);//устанавливаем новый предел массива; memmove( &items[index+1],&items[index],(count-index)*sizeof(void *) ); /*"раздвигаем" область массива, последовательно перемещая указатели: -------¬ ------¬ ------¬ ------¬ ¦ V ¦ V ¦ V ¦ V --------T-------T---+---T----+--T----+--T----+--T------------------¬ ¦ item0 ¦ item1 ¦ item2 ¦ item3 ¦ item4 ¦ item5 ¦//////////////////¦ L-------+-------+-------+-------+-------+-------+------------------- +- позиция вставляемого элемента count++;//увеличиваем счетчик; items[index] = item;//индексируем массив и вставляем элемент; } void TCollection::atPut( int index, void *item ) {//заменить элемент в массиве, заданный параметром index, на новый //(не уничтожая объект); if( index >= count )//без пояснений; return;//error(1,0); items[index] = item;//индексируем массив и вставляем элемент; } void TCollection::remove( void *item ) {//удаление объекта, заданного указателем, из списка; atRemove(indexOf(item)); //+- находим его номер; } void TCollection::removeAll() {//все исключаем из списка; count = 0; } void TCollection::error( int code, int ) {//вызывается при ошибках в работе коллекции; /*эту функцию можно переделать по своему усмотрению: например, заменить exit на return с одновременным выводом текста ошибки на экран, либо попробовать исправить ошибку ...*/ exit(212 - code); } typedef Boolean (*ccTestFunc)(void*,void*); void *TCollection::firstThat( ccTestFunc Test, void *arg ) {//ищет первый элемент, удовлетворяющий условиям, заданным функцией пользователя (Test); for( int i = 0; i < count; i++ )//проходим по всему списку; { if( Test( items[i], arg ) == True )//если условия удовлетворены, return items[i];//то возвращаем указатель на элемент; } return 0;//если нет подходящего элемента, возвращаем 0; } void *TCollection::lastThat( ccTestFunc Test, void *arg ) {//ищет последний элемент, удовлетворяющий условиям, заданным функцией пользователя (Test); for( ccIndex i = count; i > 0; i-- )//проходим по всему списку, { // начиная с последнего элемента; if( Test( items[i-1], arg ) == True )//если условия удовлетворены, return items[i-1];//то возвращаем указатель на элемент; } return 0;//если нет подходящего элемента, возвращаем 0; } typedef void (*ccAppFunc)(void*,void*); void TCollection::forEach( ccAppFunc action, void *arg ) {/*проходим по всему списку, производя действия, заданные функцией пользователя action;*/ for( ccIndex i = 0; i < count; i++ )//проходим по всему списку; action( items[i], arg );//для каждого элемента вызываем //функцию пользователя action; } void TCollection::free( void *item ) {//удаляем элемент из списка и разрушаем его; remove( item ); //удаление из списка; freeItem( item ); //разрушение; } void TCollection::freeAll() { //разрушаем все элементы массива; for( ccIndex i = 0; i < count; i++ ) freeItem( at(i) ); count = 0; } void TCollection::freeItem( void *item ) {//разрушение элемента; delete item; } #pragma warn -rvl ccIndex TCollection::indexOf(void *item) {//нахождение номера элемента в списке; for( int i = 0; i < count; i++ )//проходим по всему списку, if( item == items[i] ) //сравнивая значение указателя item return i; //со значением в массиве; // error(1,0); return -1; //если элемент не найден, вызываем ошибку; } #pragma warn .rvl int TCollection::insert( void *item ) {//заносим элемент в массив; int loc = count;//сохраняем счетчик элементов; atInsert( count, item );//добавляем в конец массива; return loc; //возвращаем номер элемента; } void TCollection::pack() {//чистим массив, т.е. удаляем "дырки" из массива (упаковываем его); void **curDst = items;//текущее свободное место массива; void **curSrc = items;//текущая позиция просмотра; void **last = items + count;//последний элемент массива; while( curSrc < last )//пока не дошли до последнего элемента { if( *curSrc != 0 )/*если указатель очередного элемента не пустой, */ *curDst++ = *curSrc;//заносим его в текущую позицию, curDst ---------¬ ------- curSrc V V --------T-------T-------T-------T-------T-------T------T-----T-----¬ ¦ item0 ¦ item1 ¦////// ¦///////¦ item4 ¦ item5 ¦//////¦item7¦/////¦ L-------+-------+-------+-------+-------+-------+------+-----+------ //в противном случае пропускаем; *curSrc++; } }//Дырки возникают при удалении элемента из массива. void TCollection::setLimit(int aLimit) {//установка предельного значения массива; if( aLimit < count ) aLimit = count; if( aLimit > maxCollectionSize) aLimit = maxCollectionSize;//максимально возможное значение; if( aLimit != limit ) { void **aItems; if (aLimit == 0 ) aItems = 0; else { aItems = new void *[aLimit];//резервируем область памяти; if( count != 0 )//если массив не пустой, то memcpy( aItems, items, count*sizeof(void *) );//копируем содержимое старого массива в новую область; } delete items;//уничтожаем старую область; items = aItems;//переопределяем переменные; limit = aLimit; } } -Из этой функции вытекает, что при установке нового предельного значения расход памяти увеличивается вдвое, а копирование старого массива в новый занимает определенное время. Поэтому при инициализации коллекции желательно точнее задавать размер массива и величину, на которую он будет расти для избежания таких расходов памяти и времени. void TCollection::write( gf_wstream& os ) {//функция записи коллекции в поток; os << count << limit << delta;//записываем переменные; for( ccIndex idx = 0; idx < count; idx++ )//проходим по всему writeItem( items[idx], os );//списку и вызываем виртуальную функцию записи элементов массива; } void *TCollection::read( gf_rstream& is ) {//функция чтения массива из потока; int limit; is >> count >> limit >> delta; limit = 0; setLimit(limit); for( ccIndex idx = 0; idx < count; idx++ ) items[idx] = readItem( is ); shouldDelete=True; return this; } //............................................................... GF_Object* TCollection::build() { return new TCollection(buildInit); } TCollection::TCollection(Building) {//конструктор, вызываемый при работе с потоками; setName("TCollection"); setBuildFunc(build); items = 0; shouldDelete=False; } Где можно использовать коллекции ? Основное их применение - это работа со строками. Для этого создается коллекция, которая сама размещает строки в динамической памяти и манипулирует ими. Можно создать отсортированную коллекцию, позволяющую располагать элементы в определенном порядке, заданном специальной функцией. Работа с "мышкой" и клавиатурой. --------¬ Из всего набора функций, предоставляемых драйвером мыши, ¦ мышка ¦ наиболее часто я употребляю только четыре. L-------- Int 33h Функция 1 (01h) Включить (высветить) курсор мышки Вход: AX = 0001h; Функция 2 (02h) Выключить (погасить) курсор мышки Вход: AX = 0002h; Функция 12 (0Сh) Задать адрес подпрограммы обработки мышки Вход: AX = 000Сh; СX = маска вызова вызов при перемещении --------------+ вызов при нажатии левой кнопки ------------+ | вызов при отпускании левой кнопки ----------+ | | вызов при нажатии правой кнопки --------+ | | | вызов при отпускании правой кнопки ------+ | | | | вызов при нажатии средней кнопки ----+ | | | | | <-Mouse System вызов при отпускании средней кнопки --+ | | | | | | <-Mouse System | | | | | | | ---------T-T-T-T-T-T-T-¬ ¦ резерв ¦6¦5¦4¦3¦2¦1¦0¦ L--------+-+-+-+-+-+-+-- ES:DX = адрес пользовательской процедуры обработки событий от мышки Выход: AX = маска условия вызова (такая же, как и маска вызова) BX = состояние кнопок CX = позиция курсора в строке DX = строка курсора DI = счетчик горизонтальных перемещений SI = счетчик вертикальных перемещений Функция 16 (10h) Задать область экрана , подлежащую обновлению. (т.е. скрыть курсор если он находится в этой области) Вход: AX = 0010h CX,DX = (x,y) координаты левого верхнего угла области SI,DI = (x,y) координаты правого нижнего угла области При работе с первыми двумя функциями необходимо помнить следующее: в драйвере мыши существует счетчик - индикатор видимости, который уменьшается на 1 при вызове функции 2 и увеличивается на 1 при вызове функции 1, поэтому, если Вы два раза вызвали функцию 2 (выключить курсор), то для его включения требуется два раза вызвать функцию 1 (включение курсора). Если Вы это сделаете один раз, то курсор не появится на экране. Я решаю этот вопрос написанием своих процедур включения/выключения курсора и введением своего флага видимости mVisible. Если флаг установлен, то курсор включен, если сброшен, то курсор уже выключен и, следовательно, незачем лишний раз скрывать его. void show() { asm push ax; asm push es; if( present() && !mVisible) { если мышь присутсвует и курсор выключен, asm{ то включаю его; mov AX,1 int 33h } mVisible = True; - выставляю флаг; } asm pop es; asm pop ax; } void hide() { asm push ax; asm push es; if( mVisible ) { asm{ mov AX,2 int 33h } mVisible = False; - сбрасываю флаг; } asm pop es; asm pop ax; } Функция 16 полезна при выводе и уничтожении объекта с экрана. В этом случае курсор автоматически скрывается при попадании в заданную область. Это предотвращает его напрасное выключение. Для включения курсора и сброса этой функции необходимо вызвать функцию 1. void smarthide(int x, int y, int x1, int y1) { if( buttonCount != 0) { asm{ mov AX,10h mov CX,x mov DX,y mov SI,x1 mov DI,y1 int 33h } mVisible = False; } } Рассмотрим основную, на мой взгляд, функцию 12 - перехват управления мышкой. Эта функция передает драйверу мыши адрес входа в подпрограмму обработки прерывания, вызванного событием, определенным в маске вызова функции. Следовательно, задав маску вызова, покрывающую все события мышки, мы получаем полный контроль над ней. void registerHandler( unsigned mask, void (far *func)() ) { установка новой функции обработки событий от мышки // если mask = 0xFFFF - получаем полный контроль // void (far *func)() указатель на функцию обработки мышки _AX = 12; _CX = mask; _DX = FP_OFF( func ); _ES = FP_SEG( func ); geninterrupt( 0x33 ); handlerInstalled = True; } Определим саму функцию обработки событий от мышки struct MouseEventType события от мышки ; { uchar buttons; положение кнопок; Boolean doubleClick; флаг двойного удара; TPoint where; местоположение курсора; }; TEvent eventQueue[ eventQSize ] = { {0} } массив событий от мышки eventQueue организован ввиде замкнутой очереди ---------¬ ¦ TEvent ¦ <- TEvent *eventQHead указатель на голову очереди; ¦ TEvent ¦ ¦ TEvent ¦ <- TEvent *eventQTail указатель на хвост очереди; ¦ TEvent ¦ L--------- ushort far * Ticks = (ushort far *)MK_FP( 0x0040, 0x006c ) указатель на область данных BIOS, где хранится число тиков, характкризующее системное время; ushort near eventCount = 0; количество событий в очереди; Boolean near mouseEvents = False; Boolean near mouseReverse = False; ushort near doubleDelay = 8; определяет промежуток времени (в тиках), в течение которого считать нажатие клавиш двойным ударом; ushort near repeatDelay = 8;определяет промежуток времени (в тиках), после которого генерировать автособытия удержания кнопки; ushort near autoTicks = 0; засечка времени; ushort near autoDelay = 0;в этот флаг-переменную заносится значение, указывающее на предел времени, при котором можно генерировать автособытия удержания кнопки (т.е. если в этот промежуток времени произошло не одно нажатие кнопки); MouseEventType near lastMouse; структура предыдущих событий; MouseEventType near curMouse; структура текущих событий; MouseEventType near downMouse; структура событий при нажатии кнопки; ushort near downTicks = 0; засечка времени последнего нажатия; #pragma saveregs void huge mouseInt() { unsigned flag = _AX; запомнили маску, при которой произошел вызов; MouseEventType tempMouse; временная структура событий от мышки; tempMouse.buttons = _BL; заносим данные о кнопках; tempMouse.doubleClick = False; сбрасываем флаг двойного удара; tempMouse.where.x = _CX; заносим координату мышки по X; tempMouse.where.y = _DX; заносим координату мышки по Y; if( (flag & 0x1e) != 0 && eventCount < eventQSize ) { | и +- если есть еще место в очереди +- если событие пришло от лев/прав кнопок (среднюю не контролируем); заносим информацию в хвост очереди eventQTail->what = *Ticks; заносим показания системных часов ( в дальнейшем это потребуется для определения двойного удара и для генерации автособытия удержания кнопки); eventQTail->mouse = curMouse заносим предыдущее событие; if( ++eventQTail >= eventQueue + eventQSize ) если хвост вышел за пределы массива, eventQTail = eventQueue; переопределяем его на начало массива (закольцовываем); eventCount++; увеличиваем счетчик очереди; } curMouse = tempMouse; переопределяем указатель на текущее значение; mouseIntFlag = True; выставляем флаг - "произошло событие от мышки"; } void getMouseState( TEvent & ev ) дать состояние мышки (по требованию возвращаем событие из очереди) { disable(); запретить все прерывания asm cli; if( eventCount == 0 ) если очередь пуста { ev.what = *Ticks; заносим показания системных часов; ev.mouse = curMouse; возвращаем текущее событие; } else если очередь не пуста { ev = *eventQHead; считываем событие из головы массива; if( ++eventQHead >= eventQueue + eventQSize ) если голова очереди выходит за пределы массива, eventQHead = eventQueue;-переопределяем голову на начало; eventCount--; уменьшаем счетчик очереди } enable(); разрешаем прерывания asm sti; } Cхематично взаимодействие этих двух функций следующее: eventQueue -- при возникновении прерывания заносим ---------¬ ¦ его в хвост очереди; eventQTail -> ¦ TEvent ¦ <--- mouseInt() ¦ TEvent ¦ eventQHead -> ¦ TEvent ¦ <--- getMouseState( TEvent & ev ) ¦ TEvent ¦ L- при запросе извлекаем информацию из L--------- очереди; Анализ событий производится при запросе его извне. void TEvent::getMouseEvent( TEvent& ev ) { getMouseState( ev ); дать событие от мышки if( ev.mouse.buttons == 0 && lastMouse.buttons != 0 ) { если кнопка в текущем состоянии отпущена, а в предыдущем была нажата, ev.what = evMouseUp; то выставляем флаг отпускания кнопки; lastMouse = ev.mouse; переопределяем структуру предыдущих событий; return; } if( ev.mouse.buttons != 0 && lastMouse.buttons == 0 ) { если кнопка в текущем состоянии нажата, а в предыдущем не была нажата, if( ev.mouse.buttons == downMouse.buttons && если произошло нажатие тех же кнопок, ev.mouse.where == downMouse.where && если мышь не сдвинулась, ev.what - downTicks <= doubleDelay ) если разность показаний счетчика текущего события и засечки последнего нажатия не превышает предельного значения, то ev.mouse.doubleClick = True; устанавливаем флаг двойного нажатия; downMouse = ev.mouse; autoTicks = downTicks = ev.what; сохраняем засечку времени нажатия кнопки; autoDelay = repeatDelay; выставляем флаг-переменную; ev.what = evMouseDown; ставим маску; lastMouse = ev.mouse; переопределяем структуру последнего события мышки; return; } ev.mouse.buttons = lastMouse.buttons; заносим положение кнопок; if( ev.mouse.where != lastMouse.where ) если мышь находится в { другом месте, то произошло перемещение ev.what = evMouseMove; lastMouse = ev.mouse; return; } Контролируем автособытие удержания кнопки. При работе с клавиатурой мы об этом не заботимся, т.к. эту работу за нас делает процессор, т.е. если клавиша нажата и удерживается, то он (процессор) начинает генерировать соответствующие сигналы. При работе с мышкой эта задача полностью ложится на нас (ведь мышка не имеет своего процессора). if( ev.mouse.buttons != 0 && если клавиша нажата, ev.what - autoTicks > autoDelay ) если разность между текущим значением времени и засечкой последнего нажатия кнопки больше допустимого значения, { то autoTicks = ev.what; засекаем время (промежуток между генерацией двух соседних событий удержания кнопки); autoDelay = 1; сокращаем промежуток автогенераций; ev.what = evMouseAuto; ставим флаг удержания клавиши; lastMouse = ev.mouse; переопределяем структуру; return; } } Такова в упрощенном виде структура обработки событий от мышки, реализованная фирмой Borland в пакете Turbo Vision v1.0. В заключение могу порекомендовать применять еще одну не необходимую, но достаточно полезную функцию. Функция 9 (09h) - задать форму графического курсора. Вход: AX = 0009h; BX = номер позиции "горячей точки" в строке битового изображения курсора (-16 до 16); CX = номер строки "горячей точки" в битовом изображения курсора (-16 до 16); ES:DX = указатель на битовое изображение курсора; Битовое изображение курсора представляет собой массив из 32 слов: первые 16 слов определяют маску экрана, вторые 16 слов - маску курсора. Курсор создается накладыванием маски экрана путем логического умножения (AND) ее на область экрана, лежащую под ним, и далее накладыванием на получившийся результат маски курсора путем "исключающего или" (XOR). +------------------------------------------+ | Варианты возможных сочетаний | |------------------------------------------| |маска экрана|маска курсора| результат | |------------+-------------+---------------| | 0 | 0 | 0 | | 0 | 1 | 1 | | 1 | 0 | не изменяется | | 1 | 1 | инвертируется | +------------------------------------------+ Использовать эту функцию можно в качестве дополнительной подсказки пользователю его действий. Например, у окон есть области, за которые их можно растягивать. При попадании курсора мыши в такую область можно изменять его форму, давая понять тем самым пользователю, что он находится в "горячей точке". Рассмотрим реализацию этой функции из моего пакета ObjectmiXMax. Сначала определим структуру, содержащую координаты горячей точки, и указатель на массив, содержащий маску экрана и маску курсора: struct ShapeCursor { int HotX; //гоpячая точка куpсоpа по X; int HotY; //гоpячая точка куpсоpа по Y; union //указатель на массив фоpмы; { void* shape; long lshape; }; ShapeCursor(int x, int y, void* addr); - конструктор ~ShapeCursor(); - деструктор }; void ShapeCursor::ShapeCursor(int x, int y, void* addr): HotX(x), HotY(y), shape(addr)//указатель на массив фоpмы; {} void ShapeCursor::~ShapeCursor() { delete shape; } void setShapeCursor(ShapeCursor* sc) функция, устанавливающая форму { курсора мыши asm push ax; asm push es; asm push bx; asm push cx; asm push dx; unsigned s = (( unsigned )(sc->lshape >> 16)); //сегмент массива unsigned o = ( unsigned )sc->lshape; //смещение массива unsigned x = sc->HotX; unsigned y = sc->HotY; asm{ mov ax,9; mov bx,x mov cx,y mov es,s mov dx,o int 33h pop dx; pop cx; pop bx; pop es; pop ax; } } Приведу пример работы с такой функцией. ShapeCursor* MsNormCursor; -указатель на структуру; void prepMsCursor() создает структуру ShapeCursor; { int* MsCursor = new int[2*16]; - cоздаем массив динамической памяти; if(MsCursor) если массив создан, то инициализирую его: { курсор стрелка - стандартный курсор в графическом режиме; -----------------¬ MsCursor[0] = 0x3FFF; ¦0011111111111111¦ MsCursor[1] = 0x1FFF; ¦0001111111111111¦ MsCursor[2] = 0x0FFF; ¦0000111111111111¦ MsCursor[3] = 0x07FF; ¦0000011111111111¦ MsCursor[4] = 0x03FF; ¦0000001111111111¦ MsCursor[5] = 0x01FF; ¦0000000111111111¦ MsCursor[6] = 0x00FF; ¦0000000011111111¦ MsCursor[7] = 0x007F; ¦0000000001111111¦ MsCursor[8] = 0x003F; ¦0000000000111111¦ MsCursor[9] = 0x007F; ¦0000000001111111¦ MsCursor[10] = 0x01FF; ¦0000000111111111¦ MsCursor[11] = 0x00FF; ¦0000000011111111¦ MsCursor[12] = 0xF0FF; ¦1111000011111111¦ MsCursor[13] = 0xF87F; ¦1111100001111111¦ MsCursor[14] = 0xF87F; ¦1111100001111111¦ MsCursor[15] = 0xFCFF; ¦1111110011111111¦ L----------------- -----------------¬ MsCursor[16] = 0x0000; ¦0000000000000000¦ MsCursor[17] = 0x4000; ¦0100000000000000¦ MsCursor[18] = 0x6000; ¦0110000000000000¦ MsCursor[19] = 0x7000; ¦0111000000000000¦ MsCursor[20] = 0x7800; ¦0111100000000000¦ MsCursor[21] = 0x7C00; ¦0111110000000000¦ MsCursor[22] = 0x7E00; ¦0111111000000000¦ MsCursor[23] = 0x7F00; ¦0111111100000000¦ MsCursor[24] = 0x7F00; ¦0111111100000000¦ MsCursor[25] = 0x7C00; ¦0111110000000000¦ MsCursor[26] = 0x6C00; ¦0110110000000000¦ MsCursor[27] = 0x0600; ¦0000011000000000¦ MsCursor[28] = 0x0600; ¦0000011000000000¦ MsCursor[29] = 0x0300; ¦0000001100000000¦ MsCursor[30] = 0x0300; ¦0000001100000000¦ MsCursor[31] = 0x0000; ¦0000000000000000¦ L----------------- MsNormCursor = new ShapeCursor(0,0,MsCursor);-cоздаем структуру в горячая точка по X ----- ¦ динамической памяти } горячая точка по Y ------- void main() { ShapeCursor* MsNormCursor; -указатель на структуру; setgraph(); - переходим в графический режим; initmouse(); - инициализируем мышку; prepMsCursor(); - создаем структуру ShapeCursor; setShapeCursor(MsNormCursor);- устанавливаем "свою" форму курсора; . } ВНИМАНИЕ ! Драйвер мыши (по крайней мере тот, который использую я) в графическом режиме подготавливает курсор перед выводом на экран в последних 400 байтах графической видеопамяти, т.е. с адреса 0AE70h. Поэтому не советую работать с этой областью, иначе курсор мыши будет неправильно изображаться на экране (та часть экрана под курсором мыши, которая должна оставаться без изменений, будет неправильно воспроизводиться). -------------¬ ¦ клавиатура ¦ Работа с клавиатурой тривиальна. L------------- void TEvent::getKeyEvent() { asm { вызываем функцию 1 прерывания 16h MOV AH,1; INT 16h; JNZ keyWaiting; если флаг нуля (ZF) установлен, значит }; буфер клавиатуры не пустой - идем на метку keyWaiting для выбора символа, what = evNothing; в противном случае заносим evNothing return; keyWaiting: what = evKeyDown; устанавливаем маску; asm { MOV AH,0; вызываем функцию 0 прерывания 16h; INT 16h; }; keyDown.keyCode = _AX; заносим значение; return; } Но почему нам сразу не выйти на функцию 0 прерывания 16h ? Дело в том, что эта функция аналогична функции С getch(),а результат ее действия нам известен - программа будет стоять до тех пор, пока в буфере клавиатуры не появится символ. Функция же 1 прерывания 16 просто проверяет наличие символа в буфере (не снимая его оттуда) и в зависимости от результата либо выставляет флаг ZF, либо сбрасывает его. Теперь понятно сочетание этих двух функций: 1 - сначала удостоверяемся, что буфер не пуст 2 - читаем символ из буфера Работа с дополнительной (expanded) памятью. Сначала рассмотрите схему, где показана общая структура памяти. -----------¬ Расширенная память ¦ Extended ¦<-эта память доступна ¦----------+ в защищенном pежиме; Высокая память ¦ HMA 64K ¦<-эта память доступна -- 10000h+----------+ в реальном режиме (используется ¦ ¦ ПЗУ BIOS ¦ драйвером HYMEM.SYS); ¦ F000h +----------+ Верхняя ¦ ¦ ¦<-¬эта память может использоваться память ¦ E000h +----------+ ¦в качестве окон для отображаемой UMB ¦ ¦ ¦<--памяти Expanded; 384K ¦ D000h +----------+ ¦ ¦ ¦ ¦ C000h +----------+ ¦ ¦ Видео ¦ ¦ ¦ память ¦ L- A000h +----------+ основная память ¦ ОЗУ ¦ ¦ 640K ¦ 0000h L----------- В зависимости от метода работы с памятью выше 1 Mb, ее можно разделить на расширенную (Extended) и дополнительную (Expanded). Процессоры фирмы Intel, начиная с i286, уже могут адресоваться к памяти выше 1 Mb, но теперь тормозом служит сама операционная системя MS DOS фирмы Microsoft - изначально в ней не была заложена возможность работы с адресным пространством более 1 Mb (в те времена 640 Kb казались бесконечностью). -----------¬ Если в Вашей машине понатыкано несколько лишних чипов ¦ expanded ¦ с памятью, то Вы уже можете использовать эту память L----------- как Expanded, загрузив соответсвующий драйвер (EMS40.sys, BRATEMSE.sys или что-либо подобное). Существуют и специальные платы, изначально расчитанные на работу с Expanded памятью (например, плата BOCARAM фирмы BOCA RESEARCH INC). Рассмотрим схему (Рис 1); ----------¬ ¦extended ¦ 1M ¦---------¦ ¦ . . . ¦ ---------------T--------¬ ¦ ¦ ¦ ----------T--+-----¬ ¦ E0000H ¦---------¦ ¦ ¦ -----T--+-----¬ ¦ ¦ ¦ ¦ 16K +-- ¦ ¦ ---+-----¬ ¦ ¦ ¦ ¦ ¦---------¦ ¦ ¦ ¦ ¦ ¦ ¦ ¦ ¦ ¦ 16K +---- ¦ ¦Expanded¦ ¦ ¦ ¦ 64K ¦---------¦ ¦ ¦ ¦ ¦ ¦ ¦ ¦ ¦ 16K +------ ¦ ¦ ¦ ¦ ¦ ¦ ¦---------¦ ¦ ¦ ¦ ¦ ¦ ¦ ¦ 16K +-------+ ¦ ¦ +--- D0000H ¦---------¦ ¦ ¦ +--- ¦ . . . ¦ ¦ +--- ¦ ¦ L--------- 640K ¦---------¦ ¦ ¦ ¦ RAM ¦ ¦ ¦ 0K L---------- Рис 1. До отметки 640К мы безраздельно властвуем. Пространство с адреса A0000H до C0000H занято под видеопамять. Далее начинается "черная область", которую можно отнести к царству ПЗУ. Эта область не вся занята информацией, в ней есть "дыры" (резерв) (наример, с адреса D0000H). К этим дырам можно легко добраться ( эти области находятся в пределах досягаемости MS DOS, а, следовательно, и для наших программ). Принцип работы Expanded заключается в отображении данных, находящихся выше 1M на это свободное пространство. Поэтому такую память также называют отображаемой. Как это делается ? Берется сегмент памяти равный 64K и разбивается на 4 страницы по 16K. Распределение памяти и пересылка информации происходит целыми блоками по 16K - постранично. Драйвер такой памяти играет ту же роль, что и драйвер мыши - он управляет работой. Аналогично прерываниям для работы с мышью существуют прерывания для работы с Expanded памятью (прерывание 67H). Я могу предложить свою версию обработки функций этого прерывания. Если Вас удовлетворит такая реализация функций, то можете использовать ее в своих программах. /*-------------------------------* | Object miXMax v 1.0 | | | | Copyright (c) Dima_GF | | 05.05.1992 | | Moscow 459-94-07 | |--------------------------------| | GFEMM.cpp | *-------------------------------*/ #pragma inline #include #include #include #include #include "gfemm.h" char far* BaseEMM = 0x0; unsigned char EMMInstalled = 0; Функции нижнего уровня. В них просто вызывается соответствующее прерывание. Они используются в функциях верхнего уровня. /*================================================= Function emmStatus Description: Эта функция возвращает cтатус памяти. =================================================*/ unsigned char emmStatus( void ) { asm mov ah,40h asm int 67h return _AH; }/* END of Function emmStatus */ /*================================================= Function emmBase Description: Возвращает базовый сегмент адpес EMM, т.е. определили адрес "дыры". =================================================*/ unsigned emmBase( void ) { asm mov ah,41h asm int 67h return _BX; }/* END of Function emmBase */ /*================================================= Function emmPageTotal Description: Возвращает общее кол-во памяти. =================================================*/ unsigned emmPageTotal( void ) { asm mov ah,42h asm int 67h return _DX; }/* END of Function emmPageTotal */ /*================================================= Function unsigned emmPageAvail Description: Сколько свободно памяти. =================================================*/ unsigned emmPageAvail( void ) { asm mov ah,42h asm int 67h return _BX; }/* END of Function emmPageAvail */ /*================================================= Function emmMemAlloc Description: выделить память в страницах по 16K. =================================================*/ unsigned emmMemAlloc( unsigned numPages ) {// +- кол-во страниц asm mov ah,43h asm mov bx,numPages asm int 67h return _DX; }/* END of Function emmMemAlloc */ /*================================================= Function emmFreeMem Description: освободить память, занимаемую обработчиком (т.е. освобождаем не постранично, а по участкам - handle - номер участка). =================================================*/ void emmFreeMem( unsigned handle ) {// +- номер обработчика asm mov ah,45h asm mov dx,handle asm int 67h }/* END of Function emmFreeMem */ /*================================================= Function emmGetMem Description: отобpазить стpаницу. Это центральная функция. Чтобы работать с дополнительной памятью, необходимо отобразить ее на область, куда можно добраться, т.е. в "дыру". После этого мы работаем с ней. Если нам понадобится другой участок памяти, то драйвер автоматически сохраняется этот, а на его место выводит новый. =================================================*/ void emmGetMem( unsigned handle, // номер обработчика (его получаем // при выделении памяти; unsigned char physPage, //физическая страница (0-4), //т.е. номер страницы в "дыре"; unsigned logicPage // логическая страница, т.е. номер блока 16K в Expanded; ) { asm mov ah,44h asm mov al,physPage asm mov bx,logicPage asm mov dx,handle asm int 67h }/* END of Function emmGetMem */ /*================================================= Function emmVersion Description: Какая веpсия драйвера EMM. Информация возвращается в BCD виде. Например, значение 32Н - соответсвует версии 3.2. =================================================*/ unsigned char emmVersion( void ) { asm mov ah,46h asm int 67h return _AL; }/* END of Function emmVersion */ /*================================================= Function emmActivHandles Description: Сколько обpаботчиков задействовано. При выделении памяти возвращается номер, под которым зарегестрирован Ваш запрос. =================================================*/ unsigned emmActivHandles( void ) { asm mov ah,4bh asm int 67h return _BX; }/* END of Function emmActivHandles */ /*================================================= Function emmNumPagesInHand Description:возвращает кол-во стpаниц, связанных с обработчиком. =================================================*/ unsigned emmNumPagesInHand( void ) { asm mov ah,4ch asm int 67h return _BX; }/* END of Function emmNumPagesInHand */ Функции верхнего уровня. С этими функциями работает пользователь. /*================================================= Function emmAlloc Description: выделяет память в EMM. =================================================*/ unsigned emmAlloc(long size ) {// +- кол-во в байтах; unsigned nPages = size/EMMMaxPgSize;// сколько это в блоках по 16K; nPages++; unsigned ret = emmMemAlloc(nPages);// размещение; if (emmStatus()) //нет ли ошибки; return EMMErrorCode; return ret; //возвращаем номер обработчика; }/* END of Function emmAlloc */ /*================================================= Function emmWriteMem Description: пеpесылка данных в EMM. =================================================*/ void emmWriteMem( unsigned handle, //обpаботчик; long dest, //позиция записи (куда); char* src, //откуда; long size //сколько; ) { long nPage = dest/EMMMaxPgSize; //номеp логич стp; dest %= EMMMaxPgSize; //c какой поз на стpанице; char* dst = BaseEMM+dest; //с какого байта; unsigned sz; if (dest) //выбиpаем остаток; { emmGetMem(handle, 0, nPage++); sz = (size>(EMMMaxPgSize-dest)) ? EMMMaxPgSize-dest : (unsigned)size; memcpy(dst,src,sz); src+=sz; //коppектиpовка; size-=sz; } for( ; size; src+=sz,size-=sz) { sz = (size>EMMMaxPgSize) ? EMMMaxPgSize : (unsigned)size; emmGetMem(handle, 0, nPage++); memcpy(BaseEMM,src,sz); } }/* END of Function emmWriteMem */ /*================================================= Function emmReadMem Description: пеpесылка данных из EMM. =================================================*/ void emmReadMem( unsigned handle, //обpаботчик; long src, //позиция чтения (откуда); char* dest, //куда; long size //сколько; ) { long nPage = src/EMMMaxPgSize; //номеp логической стpаница; src%=EMMMaxPgSize; //остаток; char* Src = BaseEMM+src; //с какого байта; unsigned sz; if (src) //выбиpаем остаток; { emmGetMem(handle, 0, nPage++); sz = (size>(EMMMaxPgSize-src)) ? EMMMaxPgSize-src : (unsigned)size; memcpy(dest,Src,sz); dest+=sz; //коppектиpовка; size-=sz; } for( ; size; dest+=sz,size-=sz) { sz = (size>EMMMaxPgSize) ? EMMMaxPgSize : (unsigned)size; emmGetMem(handle, 0, nPage++); memcpy(dest,BaseEMM,sz); } }/* END of Function emmReadMem */ /*================================================= Function emmInstalled Description: пpовеpка на пpисутствие дpайвеpа EMM. =================================================*/ int emmInstalled( void ) { int handle; if ((handle = open("EMMXXXX0", O_RDONLY)) == -1) return 0; close(handle); EMMInstalled = 1; return 1; }/* END of Function emmInstalled */ Если Вы создаете О.О.Программу, то следует оформить эти фунуции ввиде класса, управляющего дополнительной памятью. Тогда Вы получите полный контроль над распределением Expanded в Вашей программе. Так же это позволит перехватывать и анализировать ошибки, возникающие в процессе работы. Такой класс - менеджер очень полезен для корректного завершения программы, т.к. дополнительная память при завершении программы НЕ освобождается автоматически. Маленький пример на технику работы с Expanded. //************************************************************* void main() { clrscr(); if (!emmInstalled()) { cout<<"EMM не обнаpужен !"; getch(); exit(0); } cout<<"Веpсия................ "<< emmVersion()<<"\r\n" <<"Общее кол-во стpаниц.. "<< emmPageTotal()<<"\r\n" <<"Доступно.............. "<< emmPageAvail()<<"\r\n" <<"Общее кол-во памяти... "<< emmMemTotal()<<"\r\n" <<"Доступно.............. "<< emmMemAvail()<<"\r\n"; BaseEMM=(char far*)MK_FP(emmBase(),0);//определяем физический адрес // "дыры"; int err; if((err=emmStatus())!=0) { cout<<"Ошибка опpеделения базы :"< ¦this ¦ L---+------ ¦ L------ L----------- (причем результат присваивается прямоугольнику, у которого была вызвана эта функция); void Union( const TRect& r );//объединение двух прямоугольников; ----------¬ ---------------¬ ¦ this ¦ ¦ ¦ ¦ ------+----¬ ¦ ¦ ¦ ¦ ¦ r ¦ результат -> ¦ this ¦ L---+------ ¦ ¦ ¦ L----------- L--------------- (причем результат присваивается прямоугольнику, у которого была вызвана эта функция); Boolean contains( const TPoint& p ) const;/*возвращает True, если точка p входит в область прямоугольника;*/ Boolean operator == ( const TRect& r ) const; //проверка равенства двух прямоугольников; Boolean operator != ( const TRect& r ) const; //проверка неравенства двух прямоугольников; Boolean isEmpty(); /*возвращает True, если прямоугольник "пустой", т.е. либо координаты a и b равны, либо координата a.x > b.y, либо координата a.y > b.y; */ TPoint a, b; /* ¦ L-- правый нижний угол; L----- левый верхний угол;*/ }; Для ознакомления с конкретным кодом обратитесь к исходным текстам Turbo Vision фирмы Borland. Перейдем к ознакомлению с рабочим листингом заголовочного файла базового класса GF_GrObject. (приведенный класс не считается окончательным, но он вполне работоспособный [ в демонстрационной программе используется именно такая реализация]). /* -----------------------------------------------------* | Object miXMax v 1.0 | | | | Copyright (c) Dima_GF 05.03.1992 | | All Rights Reserved. | |-------------------------------------------------------| | GF_GrObject.h | *------------------------------------------------------*/ #if !defined(__COMMAND_CODES) #define __COMMAND_CODES const ushort //Константы : /* State masks характеризуют текущее состояние объекта (заносятся в переменную объекта state - состояние);*/ sfVisible = 0x001,//элемент выведен на экpан; sfActive = 0x010,/*элемент "активный", т.е. может обрабатывать события (этот флаг применяется в основном в "кнопках");*/ sfSelected = 0x020,/*элемент "выбран", т.е. активные события посылаются ему; Проверяя этот флаг, объект может определить, является ли он текущим. Этот флаг обычно выставляется "хозяином" объекта.*/ sfDefault = 0x400,/*проверив этот флаг, объект (в частности "кнопка") определяет: реагировать ли ему на команду "по умолчанию"; Такая команда генерируется объектом "окно" при нажатии клавиши "ENTER" в том случае, если никто из объектов в окне ее не обработал.*/ sfLookout = 0x800,/*элемент виден не полностью; Этот флаг выставляется, если видна на экране только часть элемента, и, следовательно, обновлять данные на экране не следует. Работа этого флага хорошо показана в демонстрационной програме (пункт меню "Разное - Пересечение"). */ /* Option masks - показывают, что может делать объект; (заносятся в переменную объекта option - установки);*/ ofSelectable = 0x001,//элемент может быть выбранным; ofPreProcess = 0x010,//проверять события в фазе PreProcess; ofPostProcess = 0x020,//проверять события в фазе PostProcess; ofSelfRest = 0x400,/*сам восстанавливает экpан; При установке этого флага у объекта менеджер экрана GF_Panel не заботится о регенерации области экрана после уничтожения данного объекта.*/ // ServiceFlag mask srvInMem = 0x000,//эти флаги показывают, где искать srvInFl = 0x001,//графический образ экрана; srvInRVM = 0x002, srvInEMM = 0x003, // Standart command cmClose = 4,//команда для объекта "окно" - закрытие; cmQUIT_PROG = 5,// "Выход из пpогpаммы"); // Dialog standart command cmOK = 10, cmCancel = 11, cmYes = 12, cmNo = 13, cmDefault = 14, //команда для элемента по умолчанию; cmEcho = 15, /* "откликнись" - применяется для поиска необходимого элемента в списке (экстравагантная команда);*/ //команды для объекта GF_Panel; cmRefreshObj = 20,//обновить графическое изображение объекта(ов); cmSaveFaceObj= 21,//сохранить графический образ объекта; cmModifyObj = 22,//генерируется объектом при его эволюциях; //Event Mask forSelectedEvents = evKeyboard | evCommand; #endif //__COMMAND_CODES #ifndef _GFGROBJ_H #define _GFGROBJ_H class far TRect; class far TEvent; class far GF_Group; class far GF_Program; TPoint Pzero(); /*================================================= Class GF_GrObject =================================================*/ struct look //структура используется для определения: { //полностью ли виден объект; TRect r; void* obj;//подпись }; class GF_GrObject : public GF_Object { protected: public: TPoint origin; /*начало объекта на экране относительно своего "хозяина";*/ TPoint size; // pазмеp объекта; GF_Group *owner; /*указатель на "хозяина"; Заполняется самим "хозяином" при вставке его в свой список.*/ GF_GrObject *next; //указатель на "подобного себе"; ushort option; /*внутренние установки; Это своеобразная инструкция объекту его действий: что ему можно и что нельзя делать, кем он может быть и кем нет. Например, если выставлен флаг ofPreProcess, то в объект будут поступать события в режиме обработки phPreProcess (например, у "кнопки" выставлен этот флаг, и она получает возможность просматривать такие события и контролировать свой вызов по Alt-...).*/ ushort state; /*отражает текущее состояние объекта; например, если выставлен флаг sfVisible - значит, элемент виден на экране, если выставлен флаг sfLookout - значит, элемент частично закрыт, если выставлен флаг sfSelected - значит, элемент является выбранным в данный момент, и к нему поступают "активные" события и т.д.*/ ushort eventMask; /*на какие события откликаться: на позиционные evMouse, на активные evKeyBoard, на сообщения evMessage. Эта переменная позволяет сократить время обработки событий объектами. Обычно установлены все флаги, т.е. объект просматривает все события;*/ enum phaseType { phSelected, phPreProcess, phPostProcess }; //peжим обpaботки; ushort echoMail; //это "карман объекта", в который можно положить какую-нибудь величину или команду; Обычно он используется для обмена между объектами cообщениями. Например, пpи взаимодействии между объектами GF_TxtDraw (вывод текста на экран) и объектом GF_ScrollBar (полоса скроллинга), при этом в EchoMail помещается процентная характеристика изменения положения ползунка на полосе скроллинга или положение текста на экране.*/ char* FNrest; /*указатель на файл/область восстановления графического образа объекта (используется как самим объектом, так и менеджером экрана GF_Panel; */ ushort ServiceFlag;/*служебные пометки в процессе функционирования объекта; Особенностью этого флага является его "динамическая" природа, т.е. при сохранении объекта в потоке он не сохраняется, поэтому при начальной инициализации класса он всегда равен 0 (в том числе и при загрузке элемента из потока).*/ GF_GrObject(TRect rect);//конструктор; ~GF_GrObject();//деструктор; virtual void handleEvent(TEvent& event);//обработчик событий; virtual void terminator(); virtual void idle();//фоновые операции; virtual void getEvent(TEvent& event);//сбор событий; virtual void putEvent(TEvent& event);/*занесение события (команды) в очередь*/ virtual void draw();//прорисовка объекта на экране; virtual void hide();/*скрытие объекта (в данном классе просто сбрасывается флаг sfVisible);*/ virtual void setData(void* rec);/*заполнить данные объекта; Эта функция позволяет изменять специфические (для данного объекта) данные. Например, в объекте GF_ChoiceBox (установка опций) можно задать первоначальную установку, которая будет воспроизведена при появлении объекта на экране. virtual void getData(void* rec);//считать данные объекта; virtual ushort dataSize();/*возвращает размер данных, необходимых объекту; Дело в том, что обмен данными с элементом типа "группа" происходит с помощью составления специальной структуры, где в определенном порядке заносятся необходимые величины для объектов данной группы. Чтобы не происходило путаницы при передаче параметров в объекты, группе необходимо знать точную длину данных каждого объекта. Для этого и была введена такая функция.*/ virtual void setState(ushort aState, Boolean enable); /*установка/сброс флага в переменной state; Введение этой функции объясняется связью между действиями объекта и его текущим состоянием. Поэтому нельзя просто выставить или сбросить флаг, необходимо произвести определенные действия, связанные с этим флагом, как самого объекта, так и его хозяина. Например, при установке флага sfSelected в объекте "кнопка" необходимо отразить это на экране. */ Boolean getState(ushort aState);/*возвращает True, если флаг aState в переменной state выставлен;*/ Boolean getOption(ushort aOption);/*возвращает True, если флаг aOption в переменной option выставлен ;*/ // virtual void callError(char* str) {}; Boolean mouseInMe(TEvent& event); /возвращает True, если курсор мыши находится на территории объекта;*/ TPoint makeGlobal(TPoint point);/*пpиведение кооpдинат к глобальному виду (абсолютной величине);*/ TPoint makeLocal(TPoint point); /*пpиведение кооpдинат к локальному виду (относительно начальных координат объекта);*/ TRect makeGlRect(); /*определение "своих" координат в абсолютных величинах;*/ void clearEvent(TEvent& event); /*очистка "события", вызывается после обработки объектом события;*/ virtual ushort lookout(look* l);/*возвращает не нулевую величину, если элемент полностью виден на экране;*/ void setFNrest(char* FN,ushort u);/*устанавливает флаги в переменной ServiceFlag, в соответствии с местом сохранения своего графического образа;*/ ushort whereFNrest(){ return (ServiceFlag & srvInEMM);};/*где находится графический образ объекта;*/ TPoint dragObject(TPoint B, TRect rm);/*перемещение объекта по экрану;*/ TPoint resizeObject(TPoint B, uchar mode, TRect rs);/*изменение размеров объекта;*/ virtual void modifyObject(TEvent& event, uchar mode,// =0, TRect rmove,// =TRect(0,0,0,0), TRect rsize);// =TRect(0,0,0,0)); /*эволюции объекта: из этой функции вызываются две предыдущие функции*/ TRect correctLimit(TRect Rect,TRect compRect, uchar mode =0); /*коppектиpовка размеров объекта при его эволюциях, (если вышли за пpеделы "владельца");*/ Boolean mouseEvent(TEvent& event, ushort mask); //функции для работы с потоком; virtual void write(gf_wstream& os); virtual void* read(gf_rstream& is); static GF_Object* build(); GF_GrObject(Building); };/* END of Class GF_GrObject */ Перегрузка операции ">>" (для загрузки объекта из потока): inline gf_rstream& operator >> (gf_rstream& io, GF_GrObject& obj) { return io >> (GF_Object& )obj;} inline gf_rstream& operator >> (gf_rstream& io, GF_GrObject* &obj) { return io >> (GF_Object*&)obj;} Перегрузка операции "<<" (для сохранения объекта в потоке): inline gf_wstream& operator << (gf_wstream& io, GF_GrObject& obj) { return io << (GF_Object&)obj;} inline gf_wstream& operator << (gf_wstream& io, GF_GrObject* obj) { return io << (GF_Object*)obj;} #endif Таковы три основные класса, необходимые для успешной работы в графическом режиме. Работа с экраном. Любой графический интерфейс подразумевает прежде всего работу с окнами. В зависимости от того, как решен этот вопрос, во многом зависит успех Вашего пакета. Главная проблема, встающая перед разработчиком - это регенерация изображения. Рассмотрим три возможных решения такой задачи. Первое решение. Создание т.н. виртуального экрана - полной копии физического. Восстановление к.-л. области сводится к пересылке данных с виртуального эрана на дисплей. Основной недостаток метода - необходимость постоянного хранения большого массива данных (размер может достигать 158 Kb при работе с эраном 640x480). дисплей виртуальный экран --------------¬ --------------¬ ¦1111111111111¦ ¦1111111111111¦ ¦1111111111111¦ ¦1111111111111¦ ¦1111111111111¦ ¦1111111111111¦ ¦1111111111111¦ ¦1111111111111¦ L-------------- L-------------- Рис 1а. дисплей виртуальный экран --------------¬ --------------¬ ¦11--------¬11¦ ¦1111111111111¦ создали окно на дисплее ¦11¦2222222¦11¦ ¦1111111111111¦ ¦11¦2222222¦11¦ ¦1111111111111¦ ¦11L--------11¦ ¦1111111111111¦ L-------------- L-------------- Рис 1б. дисплей виртуальный экран --------------¬ --------------¬ ¦11--------¬11¦ <------- ¦11--------¬11¦ уничтожаем окно пересылкой ¦11¦2222222¦11¦ ¦11¦1111111¦11¦ области с теми же коорди- ¦11¦2222222¦11¦ ¦11¦1111111¦11¦ натами из виртуального ¦11L--------11¦ <------- ¦11L--------11¦ экрана. L-------------- L-------------- Рис 1в. Второй метод. Перед выводом окна на экран сохранить область под ним. Удаление окна происходит путем вывода сохраненной области на прежнее место. дисплей сохраненная --------------¬ область ¦1111111111111¦ -------> --------¬ сохранили область ¦1111111111111¦ ¦1111111¦ ¦1111111111111¦ ¦1111111¦ ¦1111111111111¦ -------> L-------- L-------------- Рис 2а. дисплей --------------¬ вывели окно ¦11--------¬11¦ ¦11¦2222222¦11¦ ¦11¦2222222¦11¦ ¦11L--------11¦ L-------------- Рис 2б. дисплей --------------¬ область ¦1111111111111¦ <------- --------¬ восстановили область ¦1111111111111¦ ¦1111111¦ ¦1111111111111¦ ¦1111111¦ ¦1111111111111¦ <------- L-------- L-------------- Рис 2в. Такой метод хорош. Можно спокойно создавать и уничтожать окна. Но это все-таки еще не многооконный интерфейс. Дело в том, что свободно работать можно только с верхним (последним выведенным) окном. Рассмотрим следующую ситуацию. --------------¬ ¦1111111111111¦ ¦1111111111111¦ ¦1111---------+-----¬ ¦1111¦22222222222222¦ L----+22222222222222¦ ¦22222222222222¦ ¦22222---------+----¬ ¦22222¦3333333333333¦ L-----+3333333333333¦ ¦3333333333333¦ ¦3333333333333¦ L-------------- Рис 3а. Имеется три окна (рис 3а). Пока работаем с верхним окном (окно 3), проблем не возникает: его можно безболезнено перемещать, изменять размеры и даже закрыть. Но что будет, если мы захотим поработать со вторым окном (окно 2), не уничтожая при этом третье (просто переключиться) ? Допустим, нам это удалось (рис 3б). --------------¬ ¦1111111111111¦ ¦1111111111111¦ ¦1111---------+-----¬ ¦1111¦22222222222222¦ L----+22222222222222¦ ¦22222222222222¦ ¦22222222222222+----¬ ¦22222222222222¦3333¦ L-----T---------3333¦ ¦3333333333333¦ ¦3333333333333¦ L-------------- Рис 3б. Второе окно стало верхним. Теперь уничтожим его. Результат изображен на Рис 3в. --------------¬ ¦1111111111111¦ ¦1111111111111¦ ¦1111111111111¦ ¦1111111111111¦ L-------------- -----¬ ¦3333¦ ----------3333¦ ¦3333333333333¦ ¦3333333333333¦ L-------------- Рис 3в. Действительно, ведь второе окно выводилось сразу после первого, т.е. когда третьего окна еще и в помине не было, соответственно в сохраненной области под окном 2 оно никак не присутствует. Такой вариант для полноценной работы с окнами не подходит. Но не будем отбрасывать этот метод окончательно - его можно использовать при работе с меню. Структура меню ----------------------------¬ подразумевает работу одновре- ¦ Пункт 1 Пункт 2 Пункт 3 ¦ менно только с одним окном, не LT----------T---------------- уничтожив которое нельзя ¦ Пункт 1 ¦ <-------¬ спуститься на предыдущий ¦ --------+----¬ ¦ уровень. L--+ Пункт 1 ¦ L------------------------------------------¬ ¦ Пункт 2 ¦ <- с этим окном идет работа, только после ¦ ¦ Пункт 2 ¦ уничтожения его возвращаемся в предыдущее--- L------------- окно Рис 4. Также этот метод можно применять для вывода сообщений, требующих реакции пользователя. При этом нижние окна нельзя вызвать, пока не будет закрыто окно-сообщение. Примером такого окна может служить обычный запрос: " Продолжать работу ? Да/Нет.". После получения ответа окно уничтожается, и мы продолжаем работу. Для сохранения и восстановления областей экрана можно использовать функции getimage и putimage, включенные в графическую библиотеку BGI фирмы Borland. Третий метод. Рассмотрим Рис 5. --------------¬ ¦1111111111111¦ ¦1111111111111¦ ¦1111---------+-----¬ ¦1111¦22222222222222+----¬ L----+22222222222222¦3333¦ ¦22222222222222¦3333¦ ¦22222222222222¦3333¦ ¦22222222222222¦3333¦ L-----T---------3333¦ ¦3333333333333¦ ¦3333333333333¦ L-------------- Рис 5а. Есть три окна. Удалим верхнее (окно 2), закрасив его область к.- л.цветом. Получим следующую картину. --------------¬ ¦1111111111111¦ ¦1111111111111¦ ¦1111---------- ¦1111¦ -----¬ L----- ¦3333¦ ¦3333¦ ¦3333¦ ¦3333¦ ----------3333¦ ¦3333333333333¦ ¦3333333333333¦ L-------------- Рис 5б. Для обновления недостающей области последовательно пойдем по оставшимся на экране окнам, находя их ПЕРЕСЕЧЕНИЕ с ВОССТАНАВЛИВАЕМОЙ областью (Рис 5в и Рис 5г). ---------T-----¬ ---------------¬ ¦11111111¦ ¦ ¦ ---------+ +--------- ¦ ¦ ¦33333333¦ ¦ ¦ ¦ ¦33333333¦ ¦ ¦ ¦ ¦33333333¦ ¦ ¦ ¦ ¦33333333¦ L--------------- L-----+--------- Пересечение с окном 1 Пересечение с окном 3 Рис 5в. Рис 5г. Наложим второе пересечение на первое - получим недостающее изображение. ---------T-----¬ ¦11111---+-----+ +-----+33333333¦ ¦ ¦33333333¦ ¦ ¦33333333¦ ¦ ¦33333333¦ L-----+--------- Рис 5д. Теперь осталось только вывести полученную картинку на место запорченной области экрана. Т.е. суть метода такова: для регенерации изображения под удаляемым окном необходимо найти пересечение оставшихся окон с удаляемым и вывести полученную картинку на место уничтожаемого окна. Данный метод подразумевает сохранение области, занимаемой окном не до, а ПОСЛЕ его вывода на экран (вывели окно - сохранили его образ). А нахождение пересечения сводится к работе с этими образами. Как можно заметить, такой метод довольно громоздок как с т.з. вычислений, так и с т.з. хранимых данных. Но зато это уже настоящий многооконный интерфейс. Где лучше хранить образы ? Логика может быть следующей: если имеется дополнительная память (extended/expanded), то сохраняем в ней, если нет - то на диск. Если область вывода подготавливается в оперативной памяти, то в худшем случае (т.е. при регенерации всего экрана 640x480) потребуется 158 Kb. Но если подумать, то этот худший случай может уместиться и в 50 Kb (что и сделано в моем пакете, в большинстве случаев расход оперативной памяти у меня составляет 0 byte). Если имеется дополнительная память, то область можно подготавливать там (тогда и думать не надо об оперативной памяти). Не советую это делать на диске - Вы успеете выпить чашечку кофе, прежде чем увидите результат на дисплее. Перед рассмотрением кода, подготавливающего область, вспомним основную особенность EGA/VGA дисплеев. -------------¬ ¦ Панель 4 ¦ Она заключается в том, что каждому байту --+----------¬ ¦ на эране соответсвуют четыре байта ¦ Панель 3 ¦ ¦ в видеобуфере, т.е. экран получается --+----------¬ ¦ ¦ четырехслойным (слои называются панелями). ¦ Панель 2 ¦ ¦ ¦ Т.о. для сохранения одного байта с экрана --+----------¬ ¦ +-- требуется прочитать четыре байта данных ¦ Панель 1 ¦ +-- из видеобуфера (по одному байту из каждой ¦ ¦ ¦ области). См Рис 6. ¦ +-- ¦ ¦ L------------- -------------¬ ¦ ¦ ¦ Э к р а н ¦ ¦ ¦ ¦ ¦ L------------- Рис 6. Из рассуждений, приведенных выше, следует, что есть два способа хранения данных экрана в файле. Первый - пройтись последовательно по всем плоскостям, сохраняя их в файле. Т.е. файл будет выглядеть так: -----------¬-----------¬-----------¬-----------¬ ¦ панель 1 ¦¦ панель 2 ¦¦ панель 3 ¦¦ панель 4 ¦ L-----------L-----------L-----------L----------- Следовательно, восстановление сводится к последовательной загрузке этих панелей в дисплейный буфер. В результате при загрузке больших областей экрана может появиться эффект "проявления" картинки. Это не очень радует глаз. Второй способ заключается в хранении экрана построчно: ---------¬---------¬---------¬---------¬---------¬---------¬ ¦строка 1¦¦строка 1¦¦строка 1¦¦строка 1¦¦строка 2¦¦строка 2¦ ¦панель 1¦¦панель 2¦¦панель 3¦¦панель 4¦¦панель 1¦¦панель 2¦ L---------L---------L---------L---------L---------L--------- и т.д. Сначала сохраняется первая строка из всех четырех плоскостей, затем вторая из четырех плоскостей, третья и т.д. Загрузка идет соответсвующим образом: сначала загружается первая строка во все четыре плоскости, затем вторая, третья и т.д. Этот способ и примем за основу. Рассмотрим работу с таким файлом (Рис 7). | Xб | -----г======================¬ ¦ -1(start) ¦ Y ¦ ¦ ¦ -----¦----------------¬ ¦ ¦ ¦ ¦ ¦ Ya ¦ Б ¦ А ¦ ¦ ¦ ¦ ¦ ¦ -----¦------+---------+ ¦ L======================- | X | Xа | X1 | Рис 7. Пусть в файле сохранено окно Б. Из него нам необходимо вырезать (загрузить) область А. Найдем точку 1 start - начало области А: start = (Xб * 4) * (Y * 4) + (X * 4); +-пропускаем оставшиеся байты Коэффициент 4 - учитывает четырехслойность экрана. После того, как нашли начало, переходим на это место в файле и запускаем цикл: for(int i=0; i < Ya; i++) { считываем Xa*4 байтов пропускаем X1*4 + X*4 остаток---+ +-----начало } Такова в упрощенном виде схема загрузки необходимой области из файла. Сохранение происходит аналогично, заменяя считывание записью. Рассмотрим рабочий код. (код в упрощенном виде) /*================================================= Function SaveWinInFl Copyright (c) Dima_GF (Кривозубов) 05.05.1992 =================================================*/ void SaveWinInFl( TRect r, const char* FN) { r - координаты сохраняемой области (в пикселях) FN - имя файла для сохранения OutFl.open(FN,ios::binary); открываем файл в двоичном режиме | sX | asm{ StartVideo -г===========¬ mov DX,3CEH //индексный pегистp ¦ |SV | ¦ mov AL,5 //pежим чтения ¦ г=====¬--¦---- out DX,AL //3CE <- 5 ¦ ¦ r ¦ ¦ sY inc DX ¦ ¦ ¦ ¦ mov AL,0 ¦ L=====---¦---- out DX,AL L===========- } Расчет стартовой точки SV --начало видеобуфера (0xA000) ¦ char far* SV = StartVideo + r.a.x/8 + r.a.y*80; ¦ L- пропуск сверху L--расчет длины в байтах int sY = r.b.y - r.a.y; int sX = (r.b.x - r.a.x + 1)/8; for(int j=0;j < sY;j++) { for(uchar i=0;i<4;i++) { asm{ mov DX,3CEH mov AL,4 out DX,AL inc DX mov AL,i //0-1-2-3 плоскость out DX,AL } OutBufFl->sputn( SV, sX); } //for i SV+=80; пропускаем остаток + начало } //for j CleanUpEGA(); - сброс режима OutFl.close(); }/* END of Function SaveWinInFl */ /*================================================= Function ShowWinFromMem Copyright (c) Dima_GF (Кривозубов) 05.05.1992 =================================================*/ void ShowWinFromMem(TRect W, const char far* buf ) { char far* SV=StartVideo + W.a.x/8 + W.a.y*80; int sY = W.b.y - W.a.y; int sX = (W.b.x - W.a.x + 1)/8; asm{ cli mov DX,3CEh mov AL,5 out DX,AL inc DX mov AL,0 out DX,AL } for(int j=0;jsetState(sfSelected, True); } } | | -----г======================¬ ¦ -1(start) ¦ Y ¦ ¦ ¦ -----¦----------------¬ ¦ ¦ ¦ ¦ ¦ Cpy ¦ Б ¦ А ¦ ¦ ¦ ¦ ¦ ¦ -----¦------+---------+ ¦ L======================- | X | Cpy | X1 | /*================================================= Function prepareScr Copyright (c) Dima_GF (Кривозубов) 05.05.1992 =================================================*/ static void prepareScr( GF_GrObject *p, void* strt ) { st* str = (st*)strt; TRect Cpy = str->W; область восстановления в пикселях; TRect pr=p->makeGlRect(); - фактическая область, занимаемая объектом (в пикселях); Cpy.intersect(pr); находим пересечение в пикселях; if (!Cpy.isEmpty()) //если есть пеpесечение, { определяем место найденного пересечения в ФАЙЛЕ; TPoint Cpysize = Cpy.b - Cpy.a; размер области перекрытия; int X = (Cpy.a.x - pr.a.x+1)/8; // пpопуск слева в байтах; int Y = Cpy.a.y - pr.a.y; // пропуск "сверху"; long Start = ((p->size.x+1)/8*4L)*Y+X; начало области в файле; int X1 =(pr.a.x + p->size.x - Cpy.b.x+1)/8;пpопуск спpава в bt; определяем место найденного пересечения в ПОДГОТАВЛИВАЕМОЙ картинке; int Xw = (Cpy.a.x - str->W.a.x+1)/8; вычисления аналогичны int Yw = Cpy.a.y - str->W.a.y; предыдущим long StartW = ( (str->W.b.x - str->W.a.x + 1)/8L )*Yw+Xw; int X1w =(str->W.b.x - Cpy.b.x+1)/8;// пpопуск спpава в байтах; Cpysize.x = (Cpysize.x+1)/8; //в байтах (../8*4) -pазмеp по Х int szY = Cpysize.y; --- в объекте содержится имя файла с его образом InFl.open(p->FNrest,ios::binary);// откp файл; ushort sz; for(long sizebuf = Start; sizebuf; sizebuf-=sz) //пpопускаем { // начальные данные (т.е. добираемся до точки start); sz = (sizebuf>BufFlSize) ? (unsigned)BufFlSize : (unsigned)sizebuf; InBufFl->sgetn( BufFl, sz); //финт ушами - вместо seekp и подобных }//этот цикл объяснен ниже; Start=X1+X; /*- здесь start используется для подсчета количества пропускаемых байтов;*/ for( int i = 0; isgetn( buf+StartW,Cpysize.x); InBufFl->sgetn( StartTxtVideo, Start); /*финт ушами - вместо seekp и подобных*/ StartW = StartW+Cpysize.x+X1w+Xw; } //for i InFl.close(); } }/* END of Function prepareScr */ Что такое InFl и InBufFl ? Для ускорения обмена с диском необходимо применять буферизованный ввод/вывод. const int BufFlSize = 20000;// - размер буфера для дискового обмена; extern char far* BufFl; // - указатель на этот буфер; extern ofstream OutFl; // - поток вывода; extern ifstream InFl; // - поток ввода; extern filebuf* OutBufFl;-указатель на буферизованный поток вывода; extern filebuf* InBufFl;//-указатель на буферизованный поток ввода; void init() { BufFl = (char far *)farcalloc(BufFlSize,sizeof(char));/*размещаем буфер;*/ инициализируем переменные: OutFl.rdbuf()->setbuf(BufFl,BufFlSize); InFl.rdbuf()->setbuf(BufFl,BufFlSize); OutBufFl = OutFl.rdbuf(); InBufFl = InFl.rdbuf(); } Рассмотрим цикл: for(long sizebuf = Start; sizebuf; sizebuf-=sz) //пpопускаем { // начальные данные (т.е. добираемся до точки start); sz = (sizebuf>BufFlSize) ? // если размер считываемых данных // больше размера буфера, то (unsigned)BufFlSize //считываем кол-во байт равное размеру // буфера (и цикл повторяется); : (unsigned)sizebuf; // в противном случае, размер не корректируем; InBufFl->sgetn( StartTxtVideo, sz);//финт ушами - вместо seekp и подобных функций }(это особенность разработки) Такой цикл необходим, т.к. работаем с буфером ограниченного размера. Оператор: InBufFl->sgetn( StartTxtVideo, sz); финт ушами; StartTxtVideo = 0xB000 - начало буфера текстового режима - используется как мусорная яма. Но почему это не заменить обычным вызовом функции InFl.seekp(sz) ? Если Вы это сделаете, то обработка файла будет длиться не миллисекунды и даже не секунды, а десятки секунд. (у меня время работы функции подобной prepareScr доходило до ... сорока (40!) секунд.) Вы поймете почему так происходит, посмотрев исходные тексты библиотек RunTime Library фирмы Borland, а пока отнесем это к особенностям разработки потоков этой фирмой. Обращаю Ваше внимание на то, что в рассмотренных функциях мы манипулируем байтами, а не пикселями, поэтому сохраняемые и загружаемые области должны начинаться на границе байта и по ширине (координата X) должны бать кратны 8 (8 точек в одном байте). Такое ограничение может снизить привлекательность данных процедур, но поверьте, оно почти не влияет на гибкость интерфейса (поработайте с демонстрационной программой и Вы в этом убедитесь). Рассмотренный метод регенерации экрана подразумевает работы с большими массивами данных. Даже если образы окон хранить в памяти (оперативной или дополнительной), а не на диске - подготовка восстанавливаемой области требует определенных затрат времени. Если подготавливать регенирируемую область непосредственно перед ее выводом на экран (например, после получения команды на закрытие окна), то возникают паузы в работе, которые могут раздражать Вас (при работе с диском такие остановки довольно ощутимы). Какой же выход ? Ответ напрашивается сам собой - нужно подготавливать область заранее - в фоновом режиме, когда процессор ни чем не занят. В результате при уничтожении окна удаление его происходит почти мгновенно (т.к. область уже готова). Рассмотрим конкретней эту методику. Первый вопрос, встающий перед нами - как узнать заранее координаты регенерируемой области ? Это довольно просто: т.к. удаляемый объект в списке всегда верхний, то мы вправе предположить, что регенерируемая область лежит скорее всего под ним. Следовательно, необходимо пройтись по всем объектам в списке (кроме верхнего) и подготовить необходимую область. Управлением окнами занимается объект GF_Panel, поэтому загрузим его процедуру idle() задачей подготовки регенерируемой области. Особенность такой функции в том, что опрос проводится по одному объекту за один вызов процедуры. void GF_Panel::idle() { static int count; if(count > intervalIdle)/*- Не сразу приступаем к отработке задачи, { даем возможность пользователю немного поработать;*/ count = 0; if(bufFlag & fChScr) // - если выставлен флаг "экpан изменился", { // начинаем подготовку области; bufFlag &=~ fChScr; // - сбpос флага if (last) // - если список не пуст { curWin = last; // - начинаем с нижнего окна; prepareScr(curWin); // - опрос первого окна; curWin=prev(curWin);/* - ввеpх по списку; curWin указывает на следующий не обработанный объект;*/ if(curWin != last->next)//- если в списке не один элемент bufFlag |= fWork; // - выставляем флаг pаботы; else // - если в списке один элемент bufFlag |= fReady; // - выставляем флаг готовности области; } } else if(bufFlag & fWork)// - если выставлен флаг "работа" { prepareScrInRVM(curWin);/* - продолжаем подготавливать область; (по одному элементу за вызов)*/ curWin=prev(curWin); // - ввеpх по списку; if(curWin == last->next) //- если прошли по всем окнам { bufFlag &=~ fWork; // - сброс флага работы; bufFlag |= fReady; // - выставляем флаг готовности; } } } else count++;// - подсчет холостых вызовов; GF_Group::idle();// - передаем управление предку; } Остается продумать критические ситуации. Например, может прийти команда на уничтожение окна прежде, чем закончится подготовка области и т.д. Если объект не уничтожается, а модифицируется (изменяет свое положение или (и) размер), то перед выводом на экран нужно дополнить регенерируемую область образом этого объекта с учетом его эволюций. Если подготавливать выводимую область в фоновом режиме, то можно вернуться к методу виртуального экрана, т.е. создание и постоянное обновление области памяти, моделирующей экран. Тогда несколько упрощаются расчеты, т.к. отпадает необходимость определения координат регенерируемой области, но резко возрастает расход памяти (в этом случае виртуальный экран без сомнения надо помещать в дополнительную память). В заключение рассмотрим еще одну фнкцию, правда, не относящуюся к рассматриваемому вопросу подготовки области регенерации, но очень полезную. Копирование области экрана. /*================================================= Function copyScr Copyright (c) Dima_GF (Кривозубов) 05.05.1992 =================================================*/ void copyScr(TPoint p,TRect r) { // p - точка, куда переместить область r if (p!=r.a)// если действительно перемещаем область, то { p.x/=8; // выражаем величину в байтах; int x,y; // ---0xA000 char* dest= StartVideo + p.y*80+p.x;//вычисляем абсолютный адрес // (в адресном пространстве процессора) // точки назначения; char* src = StartVideo + r.a.y*80+r.a.x/8;//аналогично для точки // начала интересующей нас области; int szx = (r.b.x-r.a.x)/8; // размер по X перемещаемой области; int szy = r.b.y-r.a.y; // размер по Y перемещаемой области; uchar dir=0; //определяем направление перемещения; if ( (p.x>r.a.x/8) || (p.y>r.a.y) )//если перемещение вправо/вниз { dir = 1; // выставляем флаг; dest =StartVideo + (p.y+szy)*80+p.x; // корректировка величин src = StartVideo + r.b.y*80+r.a.x/8; } asm{// аппаратная подготовка экрана к копированию mov DX,3CEh // задаем режим копирования mov AL,5 out DX,AL inc DX mov AL,1 out DX,AL // CleanUpEGA "cброс режима"; mov dx,3C4h mov al,2 out dx,al inc dx mov al,0FFh out dx,al } if (dir) for(y = szy; y; y--) { movmemb(dest,src,szx);//замена функции movmem() фирмы Borland dest-=80; // т.к. копирование идет по байтам, а не src-=80; // по словам; } // можно обойтись только movmemb() else for(y = 0; y32000; int pPos //c какой позИции в стpоке идет вывод на экран; int Len //мах длина стpоки на стpанице (обычно 80); int maxLen //максимальная длина строки в коллекции StrColl // (для подсчета положения ползунка на //горизонтальной полосе скроллинга). Особенностью этого класса является наличие ДВУХ коллекций. Одна коллекция StrColl - содержит исходный текст. Другая коллекция virtStr - содержит текст для вывода на экран. Наличие второй коллекции объясняется главным образом необходимостью скроллинга вправо и влево, т.к. при этом строка выводится не сначала, а с определенной позиции. Подготовкой этой коллекции занимается функция prepareWin: void GF_TxtDraw::prepareWin(int cnt,//количество строк на экране; int num,//с какой строки идет вывод; int pos,//с какой позиции в строке; int len)//длина строки; { for(int i=0;igetCount();i++) {//проходим по коллекции virtStr, заполняя ее из коллекции StrColl; virtStr->atInsert(i,StrColl->getNewStr(//запрашиваем строку; num++,//номеp строки; pos, //c какой позиции в строке; len //сколькo символов пересылать; )); } } //............................................................... GF_TxtDraw::GF_TxtDraw(TRect rect,//начальные координаты вывода; int aLimit, int aDelta,//параметры коллекции //virtStr int ClrBkg//цвет фона; ):// GF_GrObject(rect), bkgClr(ClrBkg), pStr(0), //вывод с "пеpвой" стpоки pPos(0), //вывод с "пеpвой" позиции в стpоке; Len(80), //мах длина строки; maxLen(0) //определяется отдельно в процессе работы объекта; { setName("GF_TxtDraw"); setBuildFunc(build); StrColl = new GF_StrCol(10,10);//создаем коллекции в динамической virtStr = new GF_StrCollection(aLimit, aDelta);// памяти; txtClr = textClr(ClrBkg); //цвет текста; } //............................................................... void GF_TxtDraw::writeStr(int posy, int num) //позиция вывода по вертикали -- L- номер выводимой строки; {// вывод одной строки, вызывается при вертикальном скроллинге; if (posy > nStr) //если позиция неверна, то return; // выход; virtStr->atFree(0);//освобождаем массив; virtStr->atInsert(0,StrColl->getNewStr( // вводим в массив /*номеp->*/ num, нужную строку; /*c какой позиции в строке->*/ pPos, /*кол-во символов->*/ Len )); TRect r = makeGlRect(); //расчитываем координаты на экране; r.a.y = r.a.y + (posy-1)*12; r.b.y = r.a.y+12; Bar(r,bkgClr);//перед выводом закрашиваем область под строкой, //чтобы не было наложения старой строчки на новую; settextjustify(LEFT_TEXT,CENTER_TEXT);//подготавливаем setcolor(txtClr); //графическую систему settextstyle(0,0,1); //к выводу текста; outtextxy(r.a.x+1,r.a.y+6, (char*)virtStr->at(0));//вывод; } //............................................................... void GF_TxtDraw::writeChr(int posx,int num) {//вывод отдельного символа в строку (вызывается при горизонтальном //скроллинге) if (posx > Len) return; virtStr->freeAll(); prepareWin(nStr, pStr, num, 1);//подготовить virtStr; int limit = StrColl->getCount(); TRect r = makeGlRect(); //расчитываем координаты на экране; r.a.x = r.a.x + (posx-1)*8; r.b.x = r.a.x+8; Bar(r,bkgClr);//перед выводом закрашиваем область под строкой, //чтобы не было наложения старой строчки на новую; settextjustify(LEFT_TEXT,CENTER_TEXT);//подготавливаем setcolor(txtClr); //графическую систему settextstyle(0,0,1); //к выводу текста; for(int i=0;iat(i)); } //............................................................... void GF_TxtDraw::Dn() {//вызывается при скроллинге вверх; if (pStr+1getCount())//если есть еще строки в коллекции, {//то pStr++;//увеличиваем счетчик; - TRect r = makeGlRect();//расчитываем область экрана для ¦ TPoint p; // копирования ¦ p.set(r.a.x,r.a.y); ---+ r.b.y = r.a.y + nStr*12; ¦ ¦ r.a.y+=12; ¦ L copyScr(p,r); //копируем экран на строку вверх; ¦ //(см. раздел "Работа с экраном"); ¦ ¦ writeStr(nStr, pStr+nStr-1);//выводим последнюю строку; ---¬ ¦ } ---------------------- L----------------------¬ ¦ V ------+-----------V ------------¬ ------------¬ ¦ ------------¬ ¦Дима пришел¦ ^ ¦Включил ком¦ V ¦Включил ком¦ ¦Включил ком+-+¬ ¦Начал работ¦ ¦Начал работ¦ ¦Начал работ+-+- ¦Начал работ¦ "Создает про"->¦Создает про¦ L------------ L------------ ^ L------------ окно копируем подготовка результат область строки } //............................................................... void GF_TxtDraw::Up() {вызывается при скроллинге вниз; if (pStr-1>=0)//если есть еще строки в коллекции, то { pStr--;//уменьшаем счетчик; - TRect r = makeGlRect();//расчитываем область экрана для ¦ TPoint p; // копирования ---+ r.b.y = r.a.y + (nStr-1)*12; ¦ ¦ p.set(r.a.x,r.a.y+12); ¦ L copyScr(p,r); //копируем экран на строку вниз; ¦ //(см. раздел "Работа с экраном"); ¦ ¦ writeStr(1, pStr);//выводим первую строку;----¬ ¦} -------- L----------------------¬ ------+-----------¬ V ¦ V ------------¬ ------------¬ V ------------¬ ¦Включил ком+-+¬ ¦Включил ком¦ "Дима пришел"->¦Дима пришел¦ ¦Начал работ+-+- ¦Включил ком¦ ¦Включил ком¦ ¦Создает про¦ V ¦Начал работ¦ ¦Начал работ¦ L------------ L------------ ^ L------------ окно копируем подготовка результат область строки } //............................................................... void GF_TxtDraw::Rt() {вызывается при скроллинге влево; if (pPos+1 < maxLen) //если есть еще символы в строке, то { pPos++;//увеличиваем счетчик; - TRect r = makeGlRect(); ---+ TPoint p = r.a; ¦ ¦ r.a.x+=8; ¦ L copyScr(p,r);//копируем экран на позицию влево; ¦ ¦ writeChr(Len,pPos+Len-1); //выводим символы в последнюю позицию;¬ ¦ } ------------------------ L-----¬ V ----+----¬ -----------------------¬ <-+--------+- ¦ V --+--------+¬ ------------¬ ^ ------------¬ ¦Дима пришел¦ ¦има пришелл¦ "." ¦има пришел.¦ ¦Включил ком¦ ¦ключил комм¦ "п" ¦ключил комп¦ ¦Начал работ¦ ¦ачал работт¦ "а" ¦ачал работа¦ L------------ L------------ ^ L------------ окно копируем подготовка результат область строки } //............................................................... void GF_TxtDraw::Lt() {вызывается при скроллинге вправо; if(pPos-1>=0)//если не нулевая позиция, то { pPos--;//уменьшаем счетчик; - TRect r = makeGlRect(); ¦ TPoint p; ----+ p.set(r.a.x+8,r.a.y); ¦ ¦ r.b.x=r.a.x+(Len-1)*8; ¦ L copyScr(p,r); //копируем экран на позицию вправо; ¦ ¦ writeChr(1,pPos);//выводим в первую позицию;---¬ ¦ } ----------- L-----¬ V -----+---¬ -------------¬ -+--------+-> ¦ V -+--------+-¬ ------------¬ ^ ------------¬ ¦има пришел.¦ ¦иима пришел¦ "Д" ¦Дима пришел¦ ¦ключил комп¦ ¦кключил ком¦ "В" ¦Включил ком¦ ¦ачал работа¦ ¦аачал работ¦ "Н" ¦Начал работ¦ L------------ L------------ ^ L------------ окно копируем подготовка результат область строки } При уничтожении объекта уничтожаем коллекции; GF_TxtDraw::~GF_TxtDraw() { if (StrColl) delete(StrColl); if (virtStr) delete(virtStr); } PCX-формат. Разработанный фирмой ZSoft PCX-формат оказался настолько удачным, что использование его при сжатии графической информации продолжается и по сей день. Процесс сжатия данных в формат PCX очень прост: при встрече повторяющихся байт подсчитывается число их повторений, после чего в файл записывается байт-счетчик, содержащий количество повторений, а затем сам байт данных. Тем самым цепочка из нескольких повторяющихся байт кодируется двумя байтами: байтом-счетчиком и байтом данных. Если соседние байты разные, то они попадают в файл PCX-формата в неизмененном виде. не повторяющиеся байт байт байты данные байт данные данные -----+----¬ данные -----T----T----T----T--+-T--+-T--+-T----T----T--------------------¬ ¦.. ¦////¦.. ¦////¦////¦////¦////¦.. ¦////¦ и т.д. ¦ L----+----+----+----+----+----+----+----+----+--------------------- байт байт байт счетчик счетчик счетчик Чтобы различить счетчик и данные, у байта-счетчика 7 и 6 биты выставлены в 1: 7 6 5 4 3 2 1 0 --T-T-T-T-T-T-T-¬ ¦1¦1¦ ¦ ¦ ¦ ¦ ¦ ¦ LT+T+T+-+-+-+-+T- LT- L----T----- ¦ значение счетчика сигнатура (маска = 0xC0) Т.к. у байта-счетчика остается только 6 разрядов, то максимально возможное число, которое может он содержать - 63. Поэтому, если количество подряд повторяющихся байт превысит 63, то такая цепочка делится на несколько частей по 63 байта в каждой. Например, если число повторяющихся байт равно 120, то на выходе мы получим 4 байта, т.е. следующую цепочку: байты-данные -----+----¬ -----T-+--T----T-+--¬ ¦..63¦////¦..57¦////¦ L-T--+----+-T--+----- L----T----- байты-счетчики Общий формат PCX файла: заголовок д а н н ы е ---- ----T-T-T-T-T-T-T-T-T---------------T-------- ¦ ....... ¦1¦1¦ ¦ ¦ ¦ ¦ ¦ ¦ ¦ ... L--- ----¦T+T+T+-+-+-+-+T+---------------+-------- LT- L----T----- байт ¦ значение информации ¦ счетчика для сигнатура ¦ (max значение экрана (маска)=0xC0 -- 63) ¦ ¦ ¦ ¦ ¦ 128 байт¦ 1 байт ¦ 1 байт ¦ ... ЗАГОЛОВОК: Byte Name 0 Creator создатель; 1 Version 0=V2.5 версия; 2=V2.8 3=V2.8 5=V3.0 2 Encoding метод кодирования; 3 Bits/Pixel бит в пикселе; 4 Window (x1,y1)-(x2,y2) логические координаты; 12 HRes разрешение по горизонтали; 14 VRes разрешение по вертикали; 16 ColorMap массив цветов [0..15,0..2] (R,G,B - палитра); 64 reserv 65 NPlane кол-во "слоев" экрана; 66 Byte/Line кол-во байт в строке экрана; 68 Paletteinfo палетта 1= цвет; 2= оттенок серого; 70-128 reserv зарезервировано Замечу, что при использовании палитры в 256 цветов в конце файла (сразу за данными) располагается дополнительный блок размером 768 байт (где содержатся цвета от 0 до 255). Далее привожу текст кодирования и раскодирования PCX-формата на Паскале, взятый из журнала "TOOLBOX". Этот текст опробован и несколько модифицирован. Работает он надежно, хотя и медленно. {$R-,S-,I-,B-,N-,D-} UNIT PCXtools; {================================================================ +--------------------+ | PCXTools | |--------------------| | von Gustl Huder | |--------------------| | DIMA_GF | +--------------------+ !!! Исходный код взят из журнала "TOOLBOX" 5'90. Добавления, комментарии - Кривозубов Дмитрий. Москва, 1991г. Модуль для обработки PCX файлов. =================================================================} INTERFACE uses Dos; const ActivePage:word=0; var Xmin,Xmax,Ymin,Ymax : word; function BGItoPCX(gd,gm:integer; name : string) :integer; {-перекодировка BGI-формата в PCX-} type PlaneType=array[0..767] of byte; plane= ^planeType; ScanLine=array[0..3] of plane; var z:ScanLine; type PCX_Header = record {------------------запись заголовка PCX-} Creator : byte; Version :byte; Encoding :byte; Bits : byte; xmin,ymin,xmax,ymax :integer; HRes,VRes:integer; Palette :array[0..15,0..2] of byte; VMode :byte; Planes :byte; BytePerLine :integer; PaletteInfo :integer; dummy :array[0..57] of byte; end; procedure SetReadPlane(Nr:byte); {-----установка режима чтения (режим 0)} procedure SetWritePlane(Nr:byte);{-----установка режима записи (управляем маской карты)} procedure SetEGAReg(Nr,wert:byte);{----установка регистров EGA для конкретных задач} function GetPCXHeader(var PCXH:PCX_Header;name:string):integer; {cчитывание заголовка из файла PCX} function WritePCXHeader(var PCXH:PCX_Header;name:string):integer; {запись заголовка в файл PCX} function GetPcxByte(var F:file):byte; {взять байт из файла PCX} function WritePCXByte(var F:file; wert,count:byte):integer; {записать байт в файл PCX} function WritePCXLine(var F:file; var buf:plane;count:byte):integer; {записать целую строку в файл PCX} procedure DefPCXPalette(var PCXH:PCX_Header;ColType:byte); implementation const HercBase = $B000; EGABase = $A000; CGABase = $B800; BLOCKSIZE : word=32768; PCXDefaultPalette : array[0..15,0..2] of byte ={------палитра} ((0,0,0),(0,0,170),(0,170,0),(0,170,170), (170,0,0),(170,0,170),(170,170,0),(170,170,170), (85,85,85),(85,85,255),(85,255,85),(85,255,255), (255,85,85),(255,85,255),(255,255,85),(255,255,255)); var PCXbuf:array[1..32768] of byte; I,J:word; SPtr:pointer; PCXH:PCX_Header; procedure SetReadPlane(Nr:byte); begin port[$3CE]:=4; port[$3CF]:=Nr; end; procedure SetWritePlane(Nr:byte); begin port[$3C4]:=2; port[$3C5]:=1 shl Nr; end; procedure SetEGAReg(Nr,wert:byte); begin port[$3CE]:=Nr; port[$3CF]:=wert; end; function GetPCXHeader(var PCXH:PCX_Header;name:string):integer; var F:file; begin FillChar(PCXH,128,0); {обнуляем заголовок} Assign(F,name); Reset(F,1); DosError:=IOResult; If DOSError <> 0 then begin GetPCXHeader:=DosError; Exit; end; BlockRead(F,PCXH,128); {считываем заголовок} DosError:=IOResult; If DOSError <> 0 then begin GetPCXHeader:=DosError; Close(F); Exit; end; Close(F); GetPCXHeader :=IOResult; if (PCXH.version > 5) or (PCXH.encoding > 1) then GetPCXHeader :=-1; end; function WritePCXHeader(var PCXH:PCX_Header;name:string):integer; var F:file; begin Assign(F,name); Rewrite(F,1); DosError:=IOResult; If DOSError <> 0 then begin WritePCXHeader:=DosError; Exit; end; BlockWrite(F,PCXH,128); DosError:=IOResult; If DOSError <> 0 then begin WritePCXHeader:=DosError; Close(F); if IOResult<>0 then Exit; end; Close(F); WritePCXHeader :=IOResult; end; function GetPcxByte(var F:file):byte; {взять байт из файла PCX} const count:byte=0; {счетчик} wert:byte=0; {байт информации} P:word=32768; {промежуточная величина для работы со считанным блоком} endfile:boolean=false; var temp:byte; {временная величина} procedure Read_Block; {считывание блока из файла} var result :word; begin if EOF(F) then endfile:=true else begin BlockRead(F,pcxbuf,blocksize,result);{считываем блок размером blocksize с получением фактического размера result считанного блока} if result < blocksize then blocksize:=result;{если результат меньше то blocksize:=result} p:=1; {- cчетчик "обнуляем"} end; end; function get_byte:byte; {дать байт из считанного блока} begin if Endfile then get_byte:=0 else begin if p = BlockSize then {-если считанный блок исчерпан,} Read_Block {-то надо опять считать его из файла} else {в противном ...} Inc(p); get_byte:=pcxbuf[p]; {берем байт из буфера} end; end; {--основное тело--} begin if count > 0 then {если счетчик > 0 => предыдущий байт д.б. повторно записан} begin Dec(count); GetPCXByte:=wert; Exit; end; temp:=Get_byte; {получили байт} if temp and $C0 = $C0 then {проверка наложением маски: если это байт-счетчик, то...} begin count:=temp and $3F-1; {определяем его значение и} wert:=Get_byte; {---считываем след. байт, который однозначно является байтом-информации, а count= кол-ву раз его (байта-информации) повторения} end else {в противном ...} begin count:=0; wert:=temp; {считанный байт-> байт-информации } end; GetPCXByte:=wert; end; function WritePCXByte(var F:file; wert,count:byte):integer; {записать байт в файл PCX} const total:LongInt=0; begin if (count=1) and ($C0<>$C0 and wert) then {если полученный байт не "отвечает" маске байта-счетчика и счетчик =1,=> это байт-информации, причем в "единственном экземпляре"} begin Blockwrite(F,wert,1); {записываем его без предварительной записи байта-счетчика} WritePCXByte:=IOResult; total:=total+1; {общее кол-во} end else {в противном случае - этот байт-информации не в "единственном экземпляре"=> перед его записью надо записать байт-счетчик} begin count:=$C0 or count; {"метим" его (байт-счетчик)} Blockwrite(F,count,1); {и записываем} WritePCXByte:=IOResult; Blockwrite(F,wert,1); {записываем байт-информации} WritePCXByte:=IOResult; total:=total+2; {общее кол-во} end; end; function WritePCXLine(var F:file; var buf:plane; count:byte):integer; {записать целую строку в файл PCX} var this,last:byte; cptr,RunCount:byte; begin WritePCXLine:=0; last:=buf^[0]; {первоначальное значение последнего считанного байта} RunCount:=1; {счетчик "исполнения"} for cptr:=1 to count-1 do begin if buf^[cptr] = last then {если значение поступившего байта =значению предыдущего байта, то...} begin Inc(RunCount); {увелич. счетчик} if RunCount = 63 then {проверка на предельное значение } begin {если предел, то} DOSError:=WritePCXByte(F,last,RunCount); {записываем предыдущий байт и счетчик его повторений} if DOSError<>0 then begin WritePcxLine:=DOSError; Exit; end; RunCount:=0; {обнуляем счетчик} end; end else {в противном случае, т.е. значение поступившего байта не равно предыдущему } begin DOSError:=WritePCXByte(F,last,RunCount);{записываем предыдущую "серию"} if DOSError<>0 then begin WritePcxLine:=DOSError; Exit; end; last:=buf^[cptr];{присваиваем новое значение last} RunCount:=1; end; {"накапливаем" RunCount} end; if RunCount<>0 then begin DOSError:=WritePCXByte(F,last,RunCount); if DOSError<>0 then WritePcxLine:=DOSError; end; end; procedure DefPCXPalette(var PCXH:PCX_Header;ColType:byte); var I,J:integer; begin case ColType of 0: begin FillChar(PCXH.palette,48,255); FillChar(PCXH.palette,3,0); end; 1: for i:=0 to 15 do begin if Odd(i) then for j:=0 to 2 do PCXH.palette[i,j]:=240 else for j:=0 to 2 do PCXH.palette[i,j]:=0; end; 2: Move(PCXDefaultPalette,PCXH.palette,48); end; end; function BGItoPCX(gd,gm:integer; name : string) :integer; var F:file; Page:integer; procedure ErrorCheck; begin if DOSError <>0 then begin BGItoPCX:=DosError; Exit; end; end; procedure ReOpenFile; begin Assign(F,name); Reset(f,1); DosError:=IOResult; ErrorCheck; Seek(F,128); DosError:=IOResult; ErrorCheck; end; begin FillChar(PCXH,128,0); PCXH.Creator :=10; PCXH.Version :=3; PCXH.Encoding :=1; PCXH.Bits :=1; PCXH.xmin:=xmin; PCXH.ymin:=ymin; PCXH.xmax:=xmax; PCXH.ymax:=ymax; PCXH.HRes:=75; PCXH.VRes:=75; PCXH.PaletteInfo :=1; case gd of 3,4,5,9: begin case gm of 0:begin PCXH.planes:=4; PCXH.byteperline:=80; DefPCXPalette( PCXH,2); DosError:=WritePCXHeader(PCXH,name); ErrorCheck; ReOpenFile; for i:=0 to 199 do begin sptr:=Ptr(EGABase+$400*ActivePage,i*80); for page :=0 to 3 do begin SetReadPlane(Page); Move(sptr^,z[0]^,80); DosError:=WritePCXLine(f,z[0],80); ErrorCheck; end; end; end; 1:begin {-VGA} PCXH.planes:=4; PCXH.byteperline:=80; DefPCXPalette( PCXH,2); DosError:=WritePCXHeader(PCXH,name); ErrorCheck; ReOpenFile; for i:=0 to 349 do begin sptr:=Ptr(EGABase+$800*ActivePage,i*80); for page :=0 to 3 do {цикл по всем плоскостям экрана} begin SetReadPlane(Page);{уст. режима чтения плоскости} Move(sptr^,z[0]^,80); DosError:=WritePCXLine(f,z[0],80); ErrorCheck; end; end; end; 2:begin PCXH.planes:=4; PCXH.byteperline:=80; DefPCXPalette( PCXH,2); DosError:=WritePCXHeader(PCXH,name); ErrorCheck; ReOpenFile; for i:=0 to 479 do begin sptr:=Ptr(EGABase+$960*ActivePage,i*80); for page :=0 to 3 do begin SetReadPlane(Page); Move(sptr^,z[0]^,80); DosError:=WritePCXLine(f,z[0],80); ErrorCheck; end; end; end; 3:begin PCXH.planes:=1; PCXH.byteperline:=80; PCXH.version:=2; DefPCXPalette( PCXH,0); DosError:=WritePCXHeader(PCXH,name); ErrorCheck; ReOpenFile; SetReadPlane(0); for i:=0 to 349 do begin sptr:=Ptr(EGABase+$800*ActivePage,i*80); Move(sptr^,z[0]^,80); BlockWrite(f,z[0]^,80); DosError:=WritePCXLine(f,z[0],80); ErrorCheck; end; end; end; end; 7:begin PCXH.planes:=1; PCXH.byteperline:=90; PCXH.version:=2; DefPCXPalette( PCXH,0); DosError:=WritePCXHeader(PCXH,name); ErrorCheck; ReOpenFile; for i:=0 to 347 do begin sptr:=Ptr(HercBase,word((i and 3) shl 13 + 90*(i shr 2))); Move(sptr^,z[0]^,90); DosError:=WritePCXLine(f,z[0],90); ErrorCheck; end; end; 1,2:begin PCXH.planes:=1; PCXH.byteperline:=80; PCXH.bits:=2; if (gd=2) and (gm=3) then begin j:=479; PCXH.bits:=1; end else j:=199; if gm=4 then PCXH.Bits:=1; PCXH.version:=5; DefPCXPalette( PCXH,1); DosError:=WritePCXHeader(PCXH,name); ErrorCheck; ReOpenFile; for i:=0 to j do begin sptr:=Ptr(CGABase,word((i and 1) shl 13 + 80*(i shr 1))); Move(sptr^,z[0]^,80); DosError:=WritePCXLine(f,z[0],80); ErrorCheck; end; end; end; Close(F); If IOResult<>0 then Write('ОШИБКА'); end; begin GetMem(z[0],90); end. Глава 5. В двух словах. Множественное наследование. Если в Вашем пакете все объекты имеют одного предка (как в нашем случае), то применение множественного наследования приводит к конфликтам в иерархии, о которых сообщается уже на стадии компиляции. Объявление предков виртуальными как способ выхода из создавшейся ситуации, как правило, приводит к проблемам взаимодействия объектов в целом. Так что от этого свойства О.О.П. приходится отказываться. Но поверьте мне - это небольшая потеря, о которой Вы никогда и не вспомните. Отладка. Если Вы ввели в свой базовый класс данные, отражающие его имя (в нашем случае это char* ObjectName), то Вы тем самым сильно упростили отладку программы. Проверяя такое поле, можно легко понять, какой класс в данный момент является активным (это особенно важно при отслеживании последовательности обработки событий). Для просмотра класса используйте окно Inspect (Alt-F4), окно Evaluate/Modify (Ctrl-F4) и окно Watches (Ctrl-F7) [имеется ввиду встроенный отладчик BC++ 3.0]. Причем, если встроенный отладчик фирмы Borland отказывается показать запрошенные данные в окнах Inspect (Alt-F4) и Evaluate/Modify (Ctrl-F4), то почти всегда он это может сделать в окне Watches (Ctrl-F7). Почему ? Для меня это остается загадкой. Для того, чтобы вызвать полностью класс в окно Inspect (Alt-F4) встроенного отладчика, необходимо предпринять следующие шаги: -установить точку прерывания в интересующем Вас месте этого класса; -запустить программу на выполнение; -после того, как Вы попали в точку останова, вызовите окно Inspect (Alt-F4); -на запрос о вводе названия данных наберите волшебное слово this (указатель на класс); В этом окне можно просмотреть все данные и функции класса. Если Вам необходимо какое-либо конкретное поле класса, то, вызвав окно Evaluate/Modify (Ctrl-F4) или окно Watches (Ctrl-F7), наберите this->... (напрмер this->ObjectName). +- интересующее Вас поле Имейте ввиду: если используется окно Watches (Ctrl-F7), то пошаговая отладка резко замедляется (такого эффекта не наблюдается при использовании окна Inspect (Alt-F4)). Отладка О.О.Программ несколько отличается от аналогичной работы в обычной программе. В О.О.Программе необходимо обязательно задать точку останова, иначе Вы будете крутиться в одном цикле и в лучшем случае иногда попадать в фоновые операции. Как правило, точка останова задается в обработчике событий класса (handleEvent). При пошаговой отладке будьте внимательны. Если Вы не уверены, что делает данная функция, то лучше войдите в нее по клавише F7. Если в данной ситуации Вы нажмете клавишу F8, то рискуете вылететь в основной цикл и тогда придется все начинать сначала. В О.О.П. очень часто используется динамическое размещение объектов, поэтому львиная доля ошибок связана с некорректной работой в динамической области памяти. В основном, конфликты возникают из-за попытки освобождения ранее освобожденного участка памяти. В результате такого освобождения программа не завершается по ошибке, но начинает работать неправильно. Для своевременного обнаружения такой ошибки могу посоветовать ввести в программу код проверки правильности распределения кучи и вызывать его в фоновом режиме. По своему опыту знаю, что отладка О.О.Программ очень сложна и запутана, так что наберитесь терпения, прежде чем войдете в режим отладки. Turbo Vision фирмы Borland. Как я уже упоминал выше, этот пакет не для новичков в О.О.П. На нем сможет "сходу" написать интерфейс только подготовленный "оопист". Для тех же кто впервые принялся за это дело понадобится не один день для его освоения. Для успешного освоения TV сначала необходимо разобраться в идеологии пакета, которая отражает О.О. подход при создании программ в целом. Этот подход подробно изложен в первых двух главах данной книги. Поработав с этим пакетом, я могу сказать, что это очень мощный инструмент. В нем заложено много резервов (мне кажется даже слишком много). Для использования этого пакета русским программистам необходимо научить его "разговаривать" по-русски. Дело в том, что TV различает только первые 127 символов, поэтому нельзя использовать в качестве "горячих" клавиш русские буквы. Для исправления "дефектов речи" вспомним, что при нажатии клавиши генерируется скан-код. После анализа его процессором (проверяются установка клавиш сдвига и переключателей) он трансформируется в код символа. Не путайте скан-код и код символа. Так, при нажатии клавиши "Z" может выдаваться 4 разных кода символа: это зависит от того, на какой клавиатуре Вы работаете (рус/лат), а также от того, нажата ли клавиша сдвига (Shift). В зависимости от этого Вы получите либо "Z", либо "z", либо "Я", либо "я", но во ВСЕХ случаях будет генерироваться ОДИН И ТОТ ЖЕ скан-код. Существует два набора кодов: код ASCII и расширенные коды. Код ASCII - это однобайтное число, содержащее номер символа. Этот код генерируется при нажатиии обычной клавиши. Расширенный код - это двухбайтное число, причем первый байт содержит 0, а второй - номер расширенного кода. Расширенные коды генерируются тогда, когда клавиша не имеет символьного представления (например, F1) или при комбинации с клавишей Alt-. Рассмотрим, как обрабатывают "горячие" клавиши ребята из фирмы Borland. /*================================================= Function getAltChar (tvtext2.cpp [TV Borland]) =================================================*/ //Создаются два массива //Первый определяет буквы: static const char altCodes1[] = "QWERTYUIOP\0\0\0\0ASDFGHJKL\0\0\0\0\0ZXCVBNM"; //Второй определяет цифры: static const char altCodes2[] = "1234567890-="; char getAltChar(ushort keyCode) { //по скан-коду определяется символ; if ((keyCode & 0xff) == 0) //если поступил рассширенный код { //(т.е. первый байт равняется 0), то ushort tmp = (keyCode >> 8);//сдвигаем поступившую величину // вправо, получая номер расширенного кода; if( tmp == 2 )//"ловим" комбинацию ----------->----------+ return '\xF0'; // special case to handle alt-Space else if( tmp >= 0x10 && tmp <= 0x32 )//если код соответсвует // символу, то ищем его в первом массиве; return altCodes1[tmp-0x10]; // alt-letter возврат; else if( tmp >= 0x78 && tmp <= 0x83 )//если код соответсвует // числу, то ищем его во втором массиве; return altCodes2[tmp - 0x78]; // alt-number } return 0; //возвращаем 0, если поступил не расширенный код; }/* END of Function getAltChar */ /*================================================= Function getAltCode (tvtext2.cpp [TV Borland]) =================================================*/ ushort getAltCode(char c) { //нахождение кода по символу; if( c == 0 ) return 0; c = toupper(c);//переводим символ в верхний регистр //(обратие на это внимание, пакет не чуствителен к регистру); if(unsigned(c)=='\xF0' )//"ловим" комбинацию ------>--+ return 0x200; // special case to handle alt-Space for( int i = 0; i < sizeof( altCodes1 ); i++)//проходим по всему if( altCodes1[i] == c )//массиву и ищем символ; return (i+0x10) << 8;/*результат сдвигаем влево, имитируя расширенный код (это необходимо для использования констант, определенных в файле TKeys.h, которые обозначают расширенные коды)*/ for( i = 0; i < sizeof( altCodes2); i++)//повторяем предыдущую if (altCodes2[i] == c) //операцию со вторым массивом; return (i+0x78) << 8; return 0; //символ не найден; }/* END of Function getAltCode */ Теперь Вам ясно, как заставить TV понимать русские буквы. Для этого надо просто-напросто изменить данные в первом массиве: static const char altCodes1[] = "ЙЦУКЕНГШЩЗ\0\0\0\0ФЫВАПРОЛД\0\0\0\0\0ЯЧСМИТЬ"; В этом случае мы лишаемся символов латинского алфавита и нескольких русских букв (Х,Ъ,Ж,Э,Б,Ю), но зато малыми усилиями достигаем цели. Если Вас не устраивают такие потери, то Вам придется изменить код и логику обработки "горячих" клавиш. При всех преимуществах пакета TV возникает ряд вопросов, на которые трудно найти ответы. Например, непонятно, почему в TV не используются классы, поставляемые вместе с компиляторами семейства BC++, а все пишется заново ? Мало того, что это сбивает программистов, но это еще и засоряет программу, т.к. полезный код (наполняющий оболочку TV) пишется все тем же пользователем, который, вполне вероятно, для этого будет использовать классы, поставляемые с компилятором (которые отчасти дублируют классы TV). Непонятно также, зачем понадобилось создателям пакета TV для С++ переписывать потоки(этот код разбирается в разделе "Как изобрести велосипед"). Почему они не оставили ofstream, ifstrem, и т.д. в неизменном виде (достаточно было их только дополнить необходимыми функциями). Сам пакет TV сделан с избыточным запасом функциональности. Некоторые флаги можно просто исключить, некоторые объединить. Есть функции, которые совсем не вписываются в общую структуру пакета. У меня создалось впечатление, что TV для С++ не писался, а переписывался с TV для Turbo Pascal. Некоторые приемы кажутся не естественными для С++ и вполне приемлемыми для Turbo Pascal. Для тех, кто хочет сделать собственный О.О. пакет, имея перед собой исходные тексты Turbo Vision фирмы Borland, советую не увлекаться "перерисовкой" кода из TV. Вы рискуете потерять собственный стиль программирования, хотя многие вещи можно сделать по-другому и не менее элегантно и красиво. Примером может служить материал, изложенный в разделе "Сохранение объетов в потоке и загрузка объектов из потока", где показано, как можно упростить процесс сохранения и загрузки объектов. В моем пакете сохранены названия основных функций, но реализация их в большинстве случаев отличается от реализации в пакете Turbo Vision (например, полосы скроллинга у меня спроектированы по-другому, что позволило сократить число констант, используемых этим классом, до четырех против девяти в TV). Из этого не следует, что не нужно изучать исходные тексты, написанные профессионалом. Есть вещи, которые упростить или улучшить просто невозможно (например, функция forEach или at из объекта TGroup). Придерживайтесь "золотой середины".